React 代码复用

  1. Render Props
  2. HOC 高阶组件
  3. React hooks

1. Render Props

render props 指的是某个值为函数的 props 值,在 React 组件之间共享代码的技术。说白了就是某个 prop 为函数形式,为了代码复用。

1
<DataProvider render={data => <div>hello {data.name}</div>} />

实例

下面是一个跟踪鼠标位置的组件:
live demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class MouseTracker extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}

handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}

render() {
const { x, y } = this.state;
return (
<div style={{ height: "100%" }} onMouseMove={this.handleMouseMove}>
<h1>Move the mouse around!</h1>
<p>
The current mouse position is ({x}, {y})
</p>
</div>
);
}
}

现在的问题是我们如何复用这个逻辑。如果其他组件也需要鼠标位置信息,怎么复用?

假设我们有一个 Cat 组件需要鼠标位置信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}

handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}

render() {
return (
<div style={{ height: "100%" }} onMouseMove={this.handleMouseMove}>
<Cat mouse={this.state} />
</div>
);
}
}

这种写法虽然满足了需求,但是却没有达到复用的效果。接下来我们看看利用 render props 如何进行重构:
live demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}

handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}

render() {
const { render } = this.props;
return (
<div style={{ height: "100%" }} onMouseMove={this.handleMouseMove}>
{render(this.state)}
</div>
);
}
}

class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse render={mouse => <Cat mouse={mouse} />} />
</div>
);
}
}

可以看到,Cat 和 Fish 都很好的复用了 Mouse 组件的逻辑。
这个 render 参数也可以是其他名字,甚至是 children 参数

1
2
3
4
5
6
7
8
9
10
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse>{mouse => <Cat mouse={mouse} />}</Mouse>
</div>
);
}
}

Render Props 的模式需要注意一点,例如上边的例子,children 参数是一个匿名函数,所以每次渲染都会创建一个不一样的,导致像 PureComponent 这样的优化手段失效。为了解决这个问题,可以将 children 函数变成一个类方法

1
2
3
4
5
6
7
8
9
10
11
12
13
class MouseTracker extends React.Component {
renderMouse(mouse) {
return <Cat mouse={mouse} />;
}
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse>{this.renderMouse}</Mouse>
</div>
);
}
}

2. HOC 高阶组件

高阶组件(HOC)是 React 中用于组件逻辑复用的模式。其实如果理解了 Render Props 模式,就很容易理解 HOC 模式。拿第一版的 Mouse 举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class CatWithMouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}

handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}

render() {
return (
<div style={{ height: "100%" }} onMouseMove={this.handleMouseMove}>
<Cat mouse={this.state} />
</div>
);
}
}

class FishWithMouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}

handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}

render() {
return (
<div style={{ height: "100%" }} onMouseMove={this.handleMouseMove}>
<Fish mouse={this.state} />
</div>
);
}
}

可以看到 CatWithMouse 和 FishWithMouse 除了 render 中的一行代码不同外,其他都是重复的。

所以首先就会想到这部分提取成参数岂不是很好。

提取不同成分到 props 中

如果放到 props 里,那参数最好是个函数形式,如果props.children不是函数形式,那就访问不到this.state了。你看,一步步演变成了 render props 的形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class FishWithMouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}

handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}

render() {
return (
<div style={{ height: "100%" }} onMouseMove={this.handleMouseMove}>
// {this.props.children},如果非函数形式,很难访问到this.state
{this.props.children(this.state)}
</div>
);
}
}

提取不同成分到高阶函数中

首先先看一个例子

1
2
3
4
5
functon high(selfFunc){
return function inner(...args){
selfFunc(...args)
}
}

这个例子里,我们把selfFunc提取到高阶函数中去,然后生成一个新的函数体。
然后我们回到上边的问题中,我们把不同部分也提取到高阶函数的参数中来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function withMouse(Component) {
return class FishWithMouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}

handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}

render() {
return (
<div style={{ height: "100%" }} onMouseMove={this.handleMouseMove}>
<Component {...props} mouse={this.state} />
</div>
);
}
};
}

const CatWithMouse = withMouse(Cat);

这就是高阶组件了,其实很简单:把代码中多变的部分提取成参数,然后返回一个新的 class 即可

读到这里,有没有发现 Render PropsHOC 是很容易互相转换的。

3. React hooks

React hooks 相关内容请移步:

React Hooks入门