使用 Redux-Arena 组合 React 组件

使用 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;


这样做的理由是提高 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,就是因为需要解决组件间的状态传递问题,尤其是非父子组件的状态传递。

在上面的例子中,我们状态的传递方式如图:

内部管理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 {
  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 形式。

欢迎任何形式的意见和建议。

文章被以下专栏收录