90

使用 Redux-Arena 组合 React 组件

 6 years ago
source link: https://zhuanlan.zhihu.com/p/31622153?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK