標籤:

使用 Redux-Arena 組合 React 組件

對於 Redux-Arena 的簡要介紹,參考這篇文章。

Github 地址在此。

常規組合方式的缺陷

在 React 的各類組件庫中,有時為了提高組件的復用性,某些高階組件的children需要接收一個渲染函數,而不是一個Element。舉一個 React-Virtulized 中的 InfiniteLoader的例子(地址): InfiniteLoader 本身的render函數並不渲染任何 HTML 標籤,而是將一些控制參數傳入children,由 children 渲染出要表示的HTML標籤。

InfiniteLoader 的 children 簽名如下:

children?: (props: InfiniteLoaderChildProps) => React.ReactNode;n

這樣做的理由是提高 InfiniteLoader 組件的復用性,因為在 React-Virtulized 中存在著 Table、Grid、List等組件,這些真實渲染出HTML標籤的組件需要的Props各不相同,通過嵌套一個 Lambda 函數我們可以將 InfiniteLoader 組件的控制參數轉換為真實渲染組建所需要的 Props。

在 InfiniteLoader 給出的例子里,最後的render函數需要這樣寫:

<InfiniteLoadern isRowLoaded={this._isRowLoaded}n loadMoreRows={this._loadMoreRows}n rowCount={list.size}>n {({onRowsRendered, registerChild}) => (n <AutoSizer disableHeight>n {({width}) => (n <Listn ref={registerChild}n className={styles.List}n height={200}n onRowsRendered={onRowsRendered}n rowCount={list.size}n rowHeight={30}n rowRenderer={this._rowRenderer}n width={width}n />n )}n </AutoSizer>n )}n</InfiniteLoader>n

這種方式雖然解決了問題,但是構造出來的render函數卻非常醜陋,由於中間穿插了太多的lambda表達式,使得原本聲明式的jsx標籤顯得有些凌亂。而且這只是一個例子,在真實的業務場景下,這種lambda嵌套的組合方式很容易超過一個屏幕的寬度,不論是代碼審核還是後續維護都造成了一定程度上的困難。

使用Redux解決問題

首先我們要明白問題的本質,然後才能更好的解決它。我們之所以要在函數里嵌套lambda,就是因為需要解決組件間的狀態傳遞問題,尤其是非父子組件的狀態傳遞。

在上面的例子中,我們狀態的傳遞方式如圖:

內部管理state的傳遞

我們可以看到,registerChild 與 onRouwsRendered 相當於 InfiniteLoader的內部state,而width相當於AutoSizer的內部state,在這些state改變的時候,需要告知List進行相應的渲染,這就回到了Redux所要解決的問題——組件間狀態傳遞。

接入Redux後,流程會如下圖所示:

Redux接管的state傳遞

使用Redux-Arena改進狀態傳遞

首先我們需要使用 Redux-Arena 將 InfiniteLoader 中的 registerChild 與 onRowsRendered 從內部的 state ,遷移到 redux 中的store中,這一步需要重寫InfiniteLoader的部分源碼,將InfiniteLoader變為無狀態組件,然後將狀態轉換函數遷移到reducer/saga中。

我們最後導出的 InfiniteLoader 的 bundle 如下:

export default {n Component: InfiniteLoader,n actions,n state,n saga,n propsPicker: (n _,n { _arenaScene: actions }: ActionsDict<Actions> n ) => ({ actions }),n options: {n vReducerKey: "infiniteLoader"n }n};n

其中state包含 registerChild 與 onRowsRendered 兩個函數,這兩個函數需要在componentWillMount的時候註冊到 redux 中。

注意我們在 propsPicker 中並沒有將 registerChild 與 onRowsRendered 兩個函數傳遞到 InfiniteLoader 的 props 中,因為這兩個函數只需要在子組件中使用,InfiniteLoader 無需觀測它們的變化狀況。

而在List中,我們只需要將 registerChild 與 onRowsRendered 兩個函數從redux的store中取出來即可:

export default bundleToComponent({n Component: List,n propsPicker: (n { infiniteLoader: ilState }: anyn ) => ({n registerChild: ilState.registerChild,n onRowsRendered: ilState.onRowsRendered,n ...n })n});n

最後,我們最外層的render就可以寫成如下形式:

<InfiniteLoadern isRowLoaded={this._isRowLoaded}n loadMoreRows={this._loadMoreRows}n rowCount={list.size}>n <AutoSizer disableHeight>n {({width}) => (n <Listn ref={registerChild}n className={styles.List}n height={200}n onRowsRendered={onRowsRendered}n rowCount={list.size}n rowHeight={30}n rowRenderer={this._rowRenderer}n width_={width}n />n )}n </AutoSizer>n</InfiniteLoader>n

可以看到,我們此時少了一層Lambda,HTML標籤更加整潔了,如果我們願意的話,參照上面的流程,去掉 AutoSizer 中的 width ,我們的代碼最終可以變為下面的形式:

<InfiniteLoadern isRowLoaded={this._isRowLoaded}n loadMoreRows={this._loadMoreRows}n rowCount={list.size}>n <AutoSizer disableHeight>n <Listn ref={registerChild}n className={styles.List}n height={200}n onRowsRendered={onRowsRendered}n rowCount={list.size}n rowHeight={30}n rowRenderer={this._rowRenderer}n width={width}n />n </AutoSizer>n</InfiniteLoader>n

唯一的缺點是,將原本的內部管理的 state 遷移到 redux 中,不可避免的要改動原本的源代碼,對於開源組件我們大多還是遵循其原有的API,對於業務組件,我們已經全部替換為 Redux-Arena 形式。

歡迎任何形式的意見和建議。


推薦閱讀:

用 ReactJs 創建Mac版的 keep
巧用React Fiber中的渲染字元串新功能
如何看待 WordPress 的新項目 Calypso 將改用 Node.js + React 重寫?
React源碼筆記-虛擬dom

TAG:React | Redux |