本文介紹了如何使用 Function Composition 來將 React 專案中的 cross-cutting concerns 組合成一個 HOC,並且使用 currying 來解決 HOC 需要有條件的渲染的問題。作者提出的解決方案是將這些重複的區塊 cross-cutting concerns 透過 function composition 把多個 HOC 組合成一個大的 HOC,然後在專案中使用它,以減少重複的工作。
Background
在 React 專案中,常常會有一些重複的工作需要在每個頁面上都實現一次。比如認證、log、條件渲染和基本的 layout (導航、側邊欄等)。這些東西被稱為 cross-cutting concerns。
起初,你可能會在每個頁面上都使用這些重複的區塊 (cross-cutting concerns):
const MyPage = ({ user = {}, signIn, features = [], log }) =>
{
// Check and update user authentication status
useEffect(() => {
if (!user.isSignedIn) {
signIn();
}
}, [user]);
// Log each page component mount
useEffect(() => {
log({
type: 'page',
name: 'MyPage',
user: user.id,
});
}, []);
return <>
{
/* render the standard layout */
user.isSignedIn ?
<NavHeader>
<NavBar />
{
features.includes('new-nav-feature')
&& <NewNavFeature />
}
</NavHeader>
<div className="content">
{/* our actual page content... */}
</div>
<Footer /> :
<SignInComponent />
}
</>;
};
聰明的你可能會想要把這些重複的區塊抽出來,變成多個 Providers。但是,這樣反而會讓你的程式碼變得更複雜,因為你需要在每個頁面上都重複引入這些 Providers。而且,如果你想要新增一個新的 Provider,你還需要在每個頁面上都引入它。
const MyPage = ({ user = {}, signIn, features = [], log }) => {
return (
<>
<AuthStatusProvider>
<FeatureProvider>
<LogProvider>
<StandardLayout>
<div className="content">{/* our actual page content... */}</div>
</StandardLayout>
</LogProvider>
</FeatureProvider>
</AuthStatusProvider>
</>
);
};
Function Composition
作者提議的解決方案是將這些重複的區塊 cross-cutting concerns 透過 function composition 組合成一個又大又可以輕鬆維護的元件,這個元件叫做 HOC (Higher-Order Component)。如果任何一個區塊需要透過參數有條件的渲染,那麼就使用 currying 來處理該區塊。
HOC
HOC 是一個函數,它接收一個元件並返回一個新的元件。新的元件將會渲染原本的元件,但是添加了一些額外的功能。
// WithLogger is a HOC
const withLogger = (WrappedComponent) => {
return function LoggingProvider({ user, ...props }) {
useEffect(() => {
log({
type: "page",
name: "MyPage",
user: user.id,
});
}, []);
return <WrappedComponent {...props} />;
};
};
const MyPage = ({ user = {}, signIn, features = [], log }) => {
return <>{/* our actual page content... */}</>;
};
const MyPageWithLogger = withLogger(MyPage);
Function Composition
我們的目標是將多個 HOC 組合成一個大的 HOC,然後在我們的頁面上使用它。我們可以使用 function composition 的概念,將多個函數結合起來,產生一個新的函數。在代數中,它被表示為 (f ∘ g)(x) = f(g(x))
。在 JavaScript 中,我們可以定義一個 compose
函數來實現這個概念。
const compose =
(...fns) =>
(x) =>
fns.reduceRight((y, f) => f(y), x);
const withProviders = compose(withUser, withFeatures, withLogger, withLayout);
export default withProviders;
Currying
每個頁面可能會對 HOC 有不同的需求,所以我們需要一個方法來讓 HOC 有條件的渲染。例如,某一個頁面不想顯示 footer,那麼我們就可以透過 currying 來處理這個問題。Currying 就是一個函數,它接收一個參數並返回原本的函數。在下面的例子中,我們將 withLayout
函數 currying,然後將 showFooter
作為參數傳入獲得原本的 withLayout
函數。這時候的 withLayout
函數就可以根據 showFooter
的值來決定是否渲染 footer。
const withLayout =
({ showFooter = true }) =>
(WrappedComponent) => {
return function LayoutProvider({ features, ...props }) {
return (
<>
...
<WrappedComponent features={features} {...props} />
{showFooter && <Footer />}
</>
);
};
};
// We need to curry the withProviders function as well:
const withProviders = ({ showFooter }) =>
compose(withUser, withFeatures, withLogger, withLayout({ showFooter }));
HOC Usage
現在我們已經定義好了一個 HOC,我們可以在頁面上使用它了。我們可以透過 withProviders
來渲染頁面,並且傳入一個參數來決定是否顯示 footer。
const MyPage = ({ user = {}, signIn, features = [], log }) => {
return <>{/* our actual page content... */}</>;
};
const MyPageWithProviders = withProviders({
showFooter: false,
})(MyPage);