Skip to content

Render Props

术语render prop是指一种在React组件之间使用一个值为了函数的prop共享代码的简单技术。

具有render prop的组件接受一个函数,该函数返回一个React元素并调用它而不是实现自己的渲染逻辑。

txt
<DataProvider render ={ data => ( <h1>hello {data.target}</h1> )}>

这里的render仅仅是一个prop的名称,可以任意名称abc... 只需要和DataProvider组件中使用的时候保持一致即可。

使用Render Props解决横切关注点(Cross-Cutting-Concerns)

组件是React代码复用的主要单元,但如何分享一个组件封装到其他相同state组件的状态或行为并不总时很容易。 例如,以下组件跟踪web应用程序中鼠标的位置:

class MouseTracker extends React.Component{
    constructor(props){
        super(props)
        this.handleMouseMove=  this.handleMouseMove.bind(this);
        console.log(this)
        this.state = {
            x:0,
            y:0
        }
    }
    handleMouseMove(event){
        console.log(this)
        this.setState({
            x:event.clientX,
            y:event.clientY
        })
    }
    render(){
        return (
            <div style={{height:'100vh'}} onMouseMove={this.handleMouseMove}>
                <h1> 鼠标当前的位置:({this.state.x}, {this.state.y}) </h1>
            </div>
        )
    }
}

当光标在屏幕上移动的时候,组件在&lt;h1&gt;中显示其(x,y)坐标。 现在的问题是:我们如何在另一个组件中复用整个行为呢?换个说法,若另一个组件需要知道鼠标的位置,我们能否封装这一行为,以便轻松地与其他组件共享它? 由于组件是React中最基础的代码复用单元,现在尝试重构一部分代码使其能在&lt;Mouse&gt;组件中封装我们需要共享的行为。

class Mouse extends React.Component{
    constructor(props){
        super(props)
        this.handleMouseMove=  this.handleMouseMove.bind(this);
        console.log(this)
        this.state = {
            x:0,
            y:0
        }
    }
    handleMouseMove(event){
        console.log(this)
        this.setState({
            x:event.clientX,
            y:event.clientY
        })
    }
    render(){
        return (
            <div style={{height:'100vh'}} onMouseMove={this.handleMouseMove}>
                <h1> 鼠标当前的位置:({this.state.x}, {this.state.y}) </h1>
                  {/* ...但我们如何渲染 <p> 以外的东西? */}
            </div>
        )
    }
}

首先考虑的,插入一个子组件,然后将state传入子组件,这样子组件就获取到鼠标的位置:

class Mouse extends React.Component{
    constructor(props){
        super(props)
        this.handleMouseMove=  this.handleMouseMove.bind(this);
        console.log(this)
        this.state = {
            x:0,
            y:0
        }
    }
    handleMouseMove(event){
        console.log(this)
        this.setState({
            x:event.clientX,
            y:event.clientY
        })
    }
    render(){
        return (
            <div style={{height:'100vh'}} onMouseMove={this.handleMouseMove}>
                <h1> 鼠标当前的位置:({this.state.x}, {this.state.y}) </h1>
                  {/* ...但我们如何渲染 <p> 以外的东西? */}
                  <Cat mouse={this.state}  />
            </div>
        )
    }
}
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 }}  />
      );
    }
  }

类似这样,效果是实现了,但是我们还是无法复用这个组件,所以我们希望这个子组件是传入的,并且我们能向这个组件传递一个值作为props属性。 现在需要解决两个问题:

  1. 组件是通过props传入的
  2. 父组件传入的这个组件需要接受一个子组件传递的参数

结合这两个要求,只有传递一个函数,这个函数接受一个参数并返回接受这个参数作为prop的组件。 所以最终调用mouse的时候是这样的:

<Mouse  diyrender = {data => <Cat mouse={data}>}>

Mouse中render对应修改成这样:

    render(){
        return (
            <div style={{height:'100vh'}} onMouseMove={this.handleMouseMove}>
                <h1> 鼠标当前的位置:({this.state.x}, {this.state.y}) </h1>
                 {this.props.diyrender(this.state)}
            </div>
        )
    }

需要特别强调的是,这里的diyrender只是一个普通的prop的名称,并没有特殊含义。

完整代码:

jsx

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:100vh" onMouseMove={this.handleMouseMove}>
                <h1> 鼠标当前的位置:({this.state.x}, {this.state.y}) </h1>
                {this.props.diyrender(this.state)}
            </div>
        )
    }
}
jsx
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 }} ></img>
      );
    }
  }

使用 Props 而非 render

重要的是要记住,render prop 是因为模式才被称为 render prop ,你不一定要用名为 render 的 prop 来使用这种模式。事实上, 任何被用于告知组件需要渲染什么内容的函数 prop 在技术上都可以被称为 "render prop". 尽管之前的例子使用了 render,我们也可以简单地使用 children prop。

<Mouse children={mouse => (
  <p>鼠标的位置是 {mouse.x},{mouse.y}</p>
)} />

记住,children prop 并不真正需要添加到 JSX 元素的 "attributes" 列表中。相反,你可以直接放置到元素的内部!

<Mouse>
  {mouse => (
    <p>鼠标的位置是 {mouse.x},{mouse.y}</p>
  )}
</Mouse>

由于这一技术的特殊性,当你在设计一个类似的 API 时,你或许会要直接地在你的 propTypes 里声明 children 的类型应为一个函数。

Mouse.propTypes = {
  children: PropTypes.func.isRequired
};

注意事项

将Render Props与React.PureComponent一起使用的时候要小心 如果你在render方法里创建函数,那么使用render prop会抵消使用React.PureComponent带来的优势。因为浅比较props的时候总会得到false,并且在这种情况下每一个render对于render prop将会生成一个新的值。 例如,继续我们之前使用的 &lt;Mouse&gt; 组件,如果 Mouse 继承自 React.PureComponent 而不是 React.Component,我们的例子看起来就像这样:

class Mouse extends React.PureComponent {
  // 与上面相同的代码......
}

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

在这样的例子中,每次&lt;MouseTracker&gt;渲染,都会生成一个新的匿名函数作为Mouserender,因而在同时也抵消了继承自React.PureComponentMouse组件效果。 为了绕过这问题,我们可以定义一个实例方法作为prop:

class MouseTracker extends React.Component {
  // 定义为实例方法,`this.renderTheCat`始终
  // 当我们在渲染中使用它时,它指的是相同的函数
  renderTheCat(mouse) {
    return <Cat mouse={mouse}  />;
  }

  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={this.renderTheCat}  />
      </div>
    );
  }
}

如果你无法静态定义 prop(例如,因为你需要关闭组件的 props 和/或 state),则 &lt;Mouse&gt; 应该扩展 React.Component。