对于 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嵌套的组合方式很容易超过一个屏幕的宽度,不论是代码审核还是后续维护都造成了一定程度上的困难。
首先我们要明白问题的本质,然后才能更好的解决它。我们之所以要在函数里嵌套lambda,就是因为需要解决组件间的状态传递问题,尤其是非父子组件的状态传递。
在上面的例子中,我们状态的传递方式如图:
我们可以看到,registerChild 与 onRouwsRendered 相当于 InfiniteLoader的内部state,而width相当于AutoSizer的内部state,在这些state改变的时候,需要告知List进行相应的渲染,这就回到了Redux所要解决的问题——组件间状态传递。
接入Redux后,流程会如下图所示:
首先我们需要使用 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 形式。
欢迎任何形式的意见和建议。
来源: https://juejin.im/post/5a25498cf265da432b4aa8d0