跳至主要内容

本文介紹了如何使用 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):

MyPage.jsx
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,你還需要在每個頁面上都引入它。

MyPage.jsx
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 是一個函數,它接收一個元件並返回一個新的元件。新的元件將會渲染原本的元件,但是添加了一些額外的功能。

MyPage.jsx
// 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 函數來實現這個概念。

MyPage.jsx
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。

MyPage.jsx
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。

MyPage.jsx
const MyPage = ({ user = {}, signIn, features = [], log }) => {
return <>{/* our actual page content... */}</>;
};

const MyPageWithProviders = withProviders({
showFooter: false,
})(MyPage);