关于react.js:React高级特性之Render-Props

42次阅读

共计 6610 个字符,预计需要花费 17 分钟才能阅读完成。

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 了。参考 React 实战视频解说:进入学习

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> 组件来达成:

// 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 了。

正文完
 0