使用 Redux-Arena 組合 React 元件

微匯金融大前端發表於2017-12-04

對於 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,就是因為需要解決元件間的狀態傳遞問題,尤其是非父子元件的狀態傳遞。

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

使用 Redux-Arena 組合 React 元件
內部管理state的傳遞

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

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

使用 Redux-Arena 組合 React 元件
Redux接管的state傳遞

使用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 形式。

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


相關文章