所谓Render Props,涉及一个使用值为函数得的props在React组件间共享代码的简单技术。
携带render props的组件,这个props是一个返回一个React元素的函数并且通过调用该函数来代替其自身的渲染逻辑。
<DataProvider render={data => (<h1>Hello {data.target}</h1>)}/>
使用了该项技术的类库有ReactRouter,Downshif,React Motion等。
这篇文章我们将会讨论为什么render props是有用的,以及怎样自己写一个。
Cross-Cutting Concerns 横切点来使用Render Props
React中,组件是基本复用单元。但是对于一个组件封装另一个需要相同state的组件来说,共享state或者行为并不总是显而易见。
比如,下面的代码是追踪鼠标位置的:
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>);}}
当鼠标在屏幕移动,组件将鼠标的x,y坐标展示在p标签中。
现在的问题是:在另一组件中,我们如何复用次行为?换句话,如果一个组件需要知道鼠标位置,我们能否将这个行为封装起来,以便我们能在组建中共享起来?
既然组件是React的基本单元,那么让我们一点点尝试重构代码:用一个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事件而关联的行为,同时将(x, y)坐标存储起来。但这依旧不是真正的复用。
就比如,我们假象一个Cat组件,它在屏幕上渲染一个抓老鼠的小猫。我们期望能用《Cat mouse={{x, y}}》来告知组件老鼠的坐标,这样来决定将这张图片展示在什么地方。
一开始,你可能想将Cat渲染在Mouse中去:
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 thenwe 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技术这就来了:我们将给Mouse组件中传递一个函数的Props,该函数用来动态决定怎样渲染Render Props,这样来取代在Mouse中硬编码了一个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 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组件以及硬编码在render方法来解决这个特殊用例,我们提供名字是render的属性,这样Mouse组件可以动态的决定要渲染什么。
更正确的说,名为render的props是一个可以让组件知道要自己要渲染的东西。
这项技术使得我们共享的需求十分简单。为了达成目的,我们渲染Mouse时要用render props来告知当前鼠标的x,y坐标。
另一个有趣的事情是,大多数的高阶组件HOC可以用上这种常规组件携带render props的方法。比如说, 想要一个withMouse的HOC来替代Mouse组件,你可以简单的创造一个常规Mouse组件带上一个render props。
// 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让两种模式都得以实现。
使用Props不仅仅是render
记住这个很重要,这个模式叫“render props”,所以你没必要必须用一个名为render的 prop来使用该模式。事实上,任何一个函数props,其作用是提供组件的渲染元素,都叫render props。
尽管上面的例子都在使用render。但是我们可以简单的使用children属性。
<Mouse children={mouse => (<p>The mouse position is {mouse.x}, {mouse.y}</p>)}/>
同时要记住,children属性实际不需要写在JSX的一串属性中。你可以将它直接写在组件内部。
<Mouse>{mouse => (<p>The mouse position is {mouse.x}, {mouse.y}</p>)}</Mouse>
你在react-motion库中常见此技术。
既然该技术有点不寻常,你可能想在设计API的时候在propTypes中,明确的声明children应该是一个function。
Mouse.propTypes = {children: PropTypes.func.isRequired};
警告
在React.PureComponent中小心使用名为Render的Props
当你在render中新建一个函数,那么使用这个技术就抵消了使用React.PureComponent的优点。这是因为钱比较对于新的props来讲会返回false(不相等),而每一次渲染都会造成一个新的render props产生(箭头函数对象)。
比如,继续我们上面提到的例子Mouse组件,如果Mouse组件集成了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 willbe different on each render.*/}<Mouse render={mouse => (<Cat mouse={mouse} />)}/></div>);}}
这个例子中,每次MouseTracker渲染后,就产生新的函数对象作为Mouse的属性,这样就是一开始就抵消了Mouse集成PureComponent的作用。
绕过这个问题的办法,就是你可以定义属性的值为一个实例方法,像这样:
class MouseTracker extends React.Component {// Defined as an instance method, `this.renderTheCat` always// refers to *same* function when we use it in renderrenderTheCat(mouse) {return <Cat mouse={mouse} />;}render() {return (<div><h1>Move the mouse around!</h1><Mouse render={this.renderTheCat} /></div>);}}
那些你不能定义静态属性的例子中(比如你要关闭组件的属性后状态?),Mouse应该集成React。Component。
