React 组件复用和组合 (二)

上一篇文章,我们介绍了高阶组件(HOC),最后也提出了几个关于高阶组件的问题。这里我们接上另一种组件复用模式 render props 亦或是 funtion as child

render props

本质上,render props 就是在原有的组件上增加一个 prop 来实现不同的渲染情况,从而达到代码复用的目的(即将组件组件做为参数)。举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ShareStateComponent extends React.Component {
state = {...};
render() {
return (
<div>{this.props.render(this.state)}</div>
);
}
}
// 使用
class App extends React.Component {
render() {
<ShareStateComponent
render = {
(state) => {
<span>数据:{...state}</span>
}
}/>
}
}

StareStateComponent 内部维护了一些可复用的功能,在实例化时,通过调用名为 render 的 prop 属性函数实现了不同组件的渲染。换个更直观的方案,我们可以直接利用 this.props.children ,通过执行 this.props.children 方法,来传递必要的参数,从而实现基于共享数据的不同渲染逻辑,达到复用的目的。这种用法在 React Motion, React Router 里都有采用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
render (
<RenderProps1>
{(data1) => (
<div>
{data1}
<RenderProps2>
{(data2) => {
<RenderProps3>
({data3} => {
<div>{data2}, {data3}</div>
})
</RenderProps3>
}}
</RenderProps2>
</div>
)}
</RenderProps1>
)

通过上面的代码,我们可以看到相比高阶组件,render props 的方案,我们更能直观的观察到数据的流动,从而解决了高阶组件的嵌套问题。与高阶组件相比,render props 的开放性得到提升,原本 HOC 所做的功能抽象可以通过 render props 获取,render 方式还可以直接访问父级的一切内容:

a2.png

render props 存在的问题

render props 带来的优点:

  1. 不用担心props命名问题,在render函数中只取需要的state,数据流动更加直观
  2. 不会产生无用的组件加深层级
  3. render props模式的构建都是动态的,所有的改变都在render中触发,可以更好的利用组件内的生命周期。
  4. 能够直接访问父组件的内容,开发性更高

但 render props 也有一些不能忽视的问题:

  1. this.props.children 被重新定义为函数是否合适
  2. 渲染粒度变大,如果在属性中定义函数,浅比较下 prop 的值永远是新的,每次都将重新生成新的 prop,这将导致 React.PureComponent 不起作用
  3. renderProps 渲染的并不是 React 组件,无法为其单独使用 redux,mobx
  4. 容易产生嵌套地狱问题

当然,对于项目使用 HOC 还是 render props 应该根据不同的场景来进行渲染。
个人觉得,HOC 更倾向于封装一些复杂的操作,需要复用通用的业务状态和功能的时候使用。而 render props 比较适合抽离与业务无关但是和UI保存的状态有关的功能 (renderProps 内部管理的状态不方便从外部获取,因此只适合保存业务无关的数据,比如 Modal 显隐)。

RenderProps 工具库 react-powerplug

React PowerPlug 是利用 render props 进行更好状态管理的工具库。
在我们日常开发中,一个 Component 类中,可能有很多的 state 但是并不是每个状态都和有业务有关,比如:UI 的展示状态,受控组件的临时 value 等。

1
2
3
4
5
6
7
8
9
10
11
12
class App extends React.Component {
state = {
nameIsEdit: false,
briefIsEdit: false,
isLoding: false,
value: '',
data: {}
};
render() {
//....
}
}

这时候我们就一些常用的状态管理封装成 render props 的形式。react-powerplug 就是提供这样工具的一个类库。这里简单看下几个功能,来帮助我们更好理解 render props 的应用。

Value

该方法是用来管理值操作的工具。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
<Value initial="React">
{({ value, set, reset }) => (
<>
<Select
label="Choose one"
options={["React", "Preact", "Vue"]}
value={value}
onChange={set}
/>
<Button onClick={reset}>Reset to initial</Button>
</>
)}
</Value>

Value 中,只存储一个属性 value,并赋初始值为 initial。

方法:set reset。

  • set 回调函数触发后调用 setState 更新 value。
  • reset 就是调用 set 并传入 this.props.initial 即可。

Active

这是一个内置鼠标交互监听的容器,监听了 onMouseUp 与 onMouseDown,并依此判断 active 状态。

示例

1
2
3
4
5
6
7
<Active>
{({ active, bind }) => (
<div {...bind}>
You are {active ? "clicking" : "not clicking"} this div.
</div>
)}
</Active>

借助 Value 实现,巧妙的利用了 value,value 重命名为 active 且初始值为 false。增加了 bind 方法,借助 set 来更新状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import * as React from 'react'
import Value from './Value'
import renderProps from '../utils/renderProps'
const Active = ({ onChange, ...props }) => (
<Value initial={false} onChange={onChange}>
{({ value, set }) =>
renderProps(props, {
active: value,
bind: {
onMouseDown: () => set(true),
onMouseUp: () => set(false)
},
})
}
</Value>
)
export default Active

其他功能就不一一展示了。

render props 嵌套问题

上面过,render props 有一个缺点当我们想要组合使用的时候,可能会遇到嵌套地狱问题。在 react-powerplug 中提供了 compose 函数,来解决这一问题。

1
2
3
4
5
6
7
8
9
10
11
import { compose } from 'react-powerplug';
const AandB = compose(
<A .../>,
<B .../>
)
<AandB>
{(a, b) => {
//....
}}
</AandB>

社区也提供了 Epitath 工具来专门解决 render props 嵌套问题。细节就不看了。

React Hooks

React Hooks 是 React 16.7.0-alpha 版本推出的新特性。React Hooks 要解决的问题是状态共享,是继 render-props 和 higher-order components 之后的第三种状态共享方案,不会产生 JSX 嵌套地狱问题。之后介绍吧,现在还没空仔细体会呢。

参考资料