對於 Redux-Arena 的簡要介紹,參考這篇文章。
Github 地址在此。
常規組合方式的缺陷
在 React 的各類元件庫中,有時為了提高元件的複用性,某些高階元件的children需要接收一個渲染函式,而不是一個Element。舉一個 React-Virtulized 中的 InfiniteLoader的例子(地址): InfiniteLoader 本身的render函式並不渲染任何 HTML 標籤,而是將一些控制引數傳入children,由 children 渲染出要表示的HTML標籤。
InfiniteLoader 的 children 簽名如下:
children?: (props: InfiniteLoaderChildProps) => React.ReactNode;
複製程式碼
這樣做的理由是提高 InfiniteLoader 元件的複用性,因為在 React-Virtulized 中存在著 Table、Grid、List等元件,這些真實渲染出HTML標籤的元件需要的Props各不相同,通過巢狀一個 Lambda 函式我們可以將 InfiniteLoader 元件的控制引數轉換為真實渲染組建所需要的 Props。
在 InfiniteLoader 給出的例子裡,最後的render函式需要這樣寫:
<InfiniteLoader
isRowLoaded={this._isRowLoaded}
loadMoreRows={this._loadMoreRows}
rowCount={list.size}>
{({onRowsRendered, registerChild}) => (
<AutoSizer disableHeight>
{({width}) => (
<List
ref={registerChild}
className={styles.List}
height={200}
onRowsRendered={onRowsRendered}
rowCount={list.size}
rowHeight={30}
rowRenderer={this._rowRenderer}
width={width}
/>
)}
</AutoSizer>
)}
</InfiniteLoader>
複製程式碼
這種方式雖然解決了問題,但是構造出來的render函式卻非常醜陋,由於中間穿插了太多的lambda表示式,使得原本宣告式的jsx標籤顯得有些凌亂。而且這只是一個例子,在真實的業務場景下,這種lambda巢狀的組合方式很容易超過一個螢幕的寬度,不論是程式碼稽核還是後續維護都造成了一定程度上的困難。
使用Redux解決問題
首先我們要明白問題的本質,然後才能更好的解決它。我們之所以要在函式裡巢狀lambda,就是因為需要解決元件間的狀態傳遞問題,尤其是非父子元件的狀態傳遞。
在上面的例子中,我們狀態的傳遞方式如圖:
我們可以看到,registerChild 與 onRouwsRendered 相當於 InfiniteLoader的內部state,而width相當於AutoSizer的內部state,在這些state改變的時候,需要告知List進行相應的渲染,這就回到了Redux所要解決的問題——元件間狀態傳遞。
接入Redux後,流程會如下圖所示:
使用Redux-Arena改進狀態傳遞
首先我們需要使用 Redux-Arena 將 InfiniteLoader 中的 registerChild 與 onRowsRendered 從內部的 state ,遷移到 redux 中的store中,這一步需要重寫InfiniteLoader的部分原始碼,將InfiniteLoader變為無狀態元件,然後將狀態轉換函式遷移到reducer/saga中。
我們最後匯出的 InfiniteLoader 的 bundle 如下:
export default {
Component: InfiniteLoader,
actions,
state,
saga,
propsPicker: (
_,
{ _arenaScene: actions }: ActionsDict<Actions>
) => ({ actions }),
options: {
vReducerKey: "infiniteLoader"
}
};
複製程式碼
其中state包含 registerChild 與 onRowsRendered 兩個函式,這兩個函式需要在componentWillMount的時候註冊到 redux 中。
注意我們在 propsPicker 中並沒有將 registerChild 與 onRowsRendered 兩個函式傳遞到 InfiniteLoader 的 props 中,因為這兩個函式只需要在子元件中使用,InfiniteLoader 無需觀測它們的變化狀況。
而在List中,我們只需要將 registerChild 與 onRowsRendered 兩個函式從redux的store中取出來即可:
export default bundleToComponent({
Component: List,
propsPicker: (
{ infiniteLoader: ilState }: any
) => ({
registerChild: ilState.registerChild,
onRowsRendered: ilState.onRowsRendered,
...
})
});
複製程式碼
最後,我們最外層的render就可以寫成如下形式:
<InfiniteLoader
isRowLoaded={this._isRowLoaded}
loadMoreRows={this._loadMoreRows}
rowCount={list.size}>
<AutoSizer disableHeight>
{({width}) => (
<List
ref={registerChild}
className={styles.List}
height={200}
onRowsRendered={onRowsRendered}
rowCount={list.size}
rowHeight={30}
rowRenderer={this._rowRenderer}
width={width}
/>
)}
</AutoSizer>
</InfiniteLoader>
複製程式碼
可以看到,我們此時少了一層Lambda,HTML標籤更加整潔了,如果我們願意的話,參照上面的流程,去掉 AutoSizer 中的 width ,我們的程式碼最終可以變為下面的形式:
<InfiniteLoader
isRowLoaded={this._isRowLoaded}
loadMoreRows={this._loadMoreRows}
rowCount={list.size}>
<AutoSizer disableHeight>
<List
ref={registerChild}
className={styles.List}
height={200}
onRowsRendered={onRowsRendered}
rowCount={list.size}
rowHeight={30}
rowRenderer={this._rowRenderer}
width={width}
/>
</AutoSizer>
</InfiniteLoader>
複製程式碼
唯一的缺點是,將原本的內部管理的 state 遷移到 redux 中,不可避免的要改動原本的原始碼,對於開源元件我們大多還是遵循其原有的API,對於業務元件,我們已經全部替換為 Redux-Arena 形式。
歡迎任何形式的意見和建議。