render prop
是一个技术概念。它指的是使用值为 function 类型的 prop 来实现 React component 之间的代码共享。
如果一个组件有一个 render 属性,并且这个 render 属性的值为一个返回 React element 的函数,并且在组件内部的渲染逻辑是通过调用这个函数来完成的。那么,我们就说这个组件使用了render props
技术。
<DataProvider render={data => (
<h1>Hello {data.target}</h1>
)}/>
复制代码
不少类库都使用了这种技术,比如说:React Router 和 Downshift。
在这个文档里面,我们将会讨论为什么render props
是如此有用,你该如何编写自己的render props
组件。
正文
使用 Render Props 来完成关注点分离
在 React 中,组件是代码复用的基本单元(又来了,官方文档不断地在强调这个准则)。到目前为止,在 React 社区里面,关于共享 state 或者某些相似的行为(比如说,将一个组件封装进另一拥有相同 state 的组件)还没有一个明朗的方案。
举个例子,下面这个组件是用于在 web 应用中追踪鼠标的位置:
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() {
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
<h1>Move the mouse around!</h1>
<p>The current mouse position is ({this.state.x}, {this.state.y})</p>
</div>
);
}
}
复制代码
随着光标在屏幕上面移动,这个组件将会在文档的<p>
标签里面显示当前光标在 x,y 轴上的坐标值。
那么问题来了: 我们该如何在别的组件复用这种行为(指的是监听 mouseMove 事件,获取光标的坐标值)呢?换句话说,如果别的组件也需要知道目前光标的坐标值,那我们能不能将这种行为封装好,然后在另外一个组件里面开箱即用呢?
因为,在 React 中,组件是代码复用的基本单元(again)。那好,我们一起来重构一下代码,把我们需要复用的行为封装到<Mouse>
组件当中。
// The <Mouse> component encapsulates the behavior we need...
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}>
{/* ...but how do we render something other than a <p>? */}
<p>The current mouse position is ({this.state.x}, {this.state.y})</p>
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse />
</div>
);
}
}
复制代码
现在,<Mouse>
组件看似把所有跟监听 mousemove 事件,保存光标的坐标值等相关的行为封装在一起了。实际上,它还不能达到真正的可复用。
假设,我们需要实现这么一个组件。它需要渲染出一只用图片表示的猫去追逐光标在屏幕上移动的视觉效果。我们可能会通过向<Cat>
组件传递一个叫 mouse(它的值为{{x,y}})的 prop 来获得当前光标所在位置。
首先,我们会在<Mouse>
组件的 render 方法里面插入这个<Cat>
组件,像这样子:
class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
);
}
}
class MouseWithCat 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}>
{/* We could just swap out the <p> for a <Cat> here ... but then we would need to create a separate <MouseWithSomethingElse> component every time we need to use it, so <MouseWithCat> isn't really reusable yet. */}
<Cat mouse={this.state} />
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<MouseWithCat />
</div>
);
}
}
复制代码
这种方式的实现可能对个别的场景有用,但是,我们还是没有达成通过封装让这种行为真正地复用的目标。在别的应用场景下,每一次当我们需要获取光标在屏幕上的坐标的时候,我们都需要重新创建一个组件(例如,一个跟<MouseWithCat>
相似组件)来完成这个业务场景所对应的渲染任务。
这个时候,就轮到 render props 出场啦:相比直接把<Cat>
这个组件硬编码到<Mouse>
组件当中,刻意地去改变<Mouse>
组件的 UI 输出(也就是我们重新定义一个<MouseWithCat>
组件的原因)。更好的做法是,我们可以给<Mouse>
组件定义一个值为函数类型的 prop,让这个 prop 自己来动态地决定要在 Mouse 组件的 render 方法要渲染东西。这个值为函数类型的 prop 就是我们所说的render prop
了。
class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
);
}
}
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}>
{/* Instead of providing a static representation of what <Mouse> renders, use the `render` prop to dynamically determine what to render. */}
{this.props.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>
);
}
}
复制代码
现在,相比每一次都要重复地将<Mouse>
组件的代码复制一遍,然后将我们要渲染的东西硬编码到<Mouse>
的 render 方法中去,我们采取了一个更省力的办法。那就是给 Mouse 新增了一个 render 属性,让这个属性来决定要在<Mouse>
组件中渲染什么。
更加具体和直白地说,一个 render prop(这里不是代指技术,而是组件属性) 就是一个值为函数类型的 prop。通过这个函数,我们让挂载了这个 prop 的组件知道自己要去渲染什么。
这种技术使得我们之前想要共享的某些行为(的实现)变得非常之可移植(portable)。假如你想要得到这种行为,你只需要渲染一个带 render 属性的类<Mouse>
组件到你的组件树当中就可以了。剩下的就让这个 render prop 来获取相关的数据(通过函数形参被实例化时得到。拿上述例子来说,就是(mouse)=> <Cat mouse={mouse}>
的mouse
),然后决定如何干预这个组件的渲染。
一个很有意思的,并值得我们注意的事情是,你完全可以通过一个带 render 属性的普通组件来实现大部分的 HOC。举个例子,假如你在共享行为(监听 mousemove 事件,获得光标在屏幕上的坐标)时不想通过<Mouse>
组件来完成,而是想通过高阶组件withMouse
来完成的话,那么就可以很简单地通过创建一个带 render prop 的<Mouse>
组件来达成:
参考 React面试题详细解答
// If you really want a HOC for some reason, you can easily
// create one using a regular component with a render prop!
function withMouse(Component) {
return class extends React.Component {
render() {
return (
<Mouse render={mouse => (
<Component {...this.props} mouse={mouse} />
)}/>
);
}
}
}
复制代码
可以这么说,render props
(指技术)让 HOC 技术与其他技术(在这里,指它自己)的组合使用成为了可能。
render prop
的 prop 名不一定叫“render”
如上面的标题,你要牢牢记住,这种技术虽然叫render props
,但是 prop 属性的名称不一定非得叫“render”。实际上,只要组件上的某个属性值是函数类型的,并且这个函数通过自己的形参实例化时获取了这个组件的内部数据,参与到这个组件的 UI 渲染中去了,我们就说这个组件应用了render props
这种技术。
在上面的例子当中,我们一直在使用“render”这个名称。实际上,我们也可以轻易地换成children
这个名称!
<Mouse children={mouse => (
<p>The mouse position is {mouse.x}, {mouse.y}</p>
)}/>
复制代码
同时,我们也要记住,这个“children”prop 不一定非得罗列在在 JSX element 的“属性”列表中。它实际上就是我们平时用 JSX 声明组件时的 children,因此你也可以像以前一样把它放在组件的内部。
<Mouse>
{mouse => (
<p>The mouse position is {mouse.x}, {mouse.y}</p>
)}
</Mouse>
复制代码
在 react-motion 这个库的 API 中,你会看到这种写法的应用。
因为这种写法比较少见,所以假如你这么做了,为了让看你代码的人不产生疑惑的话,你可能需要在静态属性 propTypes 中显式地声明一下 children 的数据类型必须为函数。
Mouse.propTypes = {
children: PropTypes.func.isRequired
};
复制代码
注意点
当跟 React.PureComponent 结合使用时,要当心
如果你在组件的 render 方法里面创建了一个函数的话,然后把这个函数赋值给这个组件的 prop 的话,那么得到的结果很有可能是违背了你初衷的。怎么说呢?因为一旦你这么做了,React 在作 shallow prop comparison 的时候,new props 都会被判断为不等于 old props 的。现实是,这么做恰恰会导致在每一次 render 的调用的时候生成一个新的值给这个属性。
我们继续拿上面的<Mouse>
组件作为例子。假如<Mouse>
组件继承了 React.PureComponent 的话,我们的代码应该是像下面这样的:
class Mouse extends React.PureComponent {
// Same implementation as above...
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
{/* This is bad! The value of the `render` prop will be different on each render. */}
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
}
复制代码
在上面的代码例子当中,每一次<MouseTracker>
组件的 render 方法被调用的时候,它都会生成一个新的函数实例给<Mouse>
组件,作为“render”属性的值。然而,我们之所以继承 React.PureComponent,就是想减少<Mouse>
组件被渲染的次数。如此一来,<Mouse>
因为一个新的函数实例被迫判定为 props 已经发生改变了,于是乎进行了不必要的渲染。这与我们的让<Mouse>
组件继承 React.PureComponent 的初衷是相违背的。
为了避开(To get around)这个问题,你可以把 render prop 的值赋值为<MouseTracker>
组件实例的一个方法,这样:
class MouseTracker extends React.Component {
// Defined as an instance method, `this.renderTheCat` always
// refers to *same* function when we use it in render
renderTheCat(mouse) {
return <Cat mouse={mouse} />;
}
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse render={this.renderTheCat} />
</div>
);
}
}
复制代码
在某些场景下,你可能无法把 prop 的值静态地赋值为组件实例的某个方法(例如,你需要覆盖组件的 props 值或者 state 值,又两者都要覆盖)。那么,在这种情况下,你只能老老实实地让<Mouse>
组件去继承 React.Component 了。
评论