• Render Props
    • 使用 Render Props 来解决横切关注点(Cross-Cutting Concerns)
    • 使用 Props 而非 render
    • 注意事项
      • 将 Render Props 与 React.PureComponent 一起使用时要小心

    Render Props

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

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

    1. <DataProvider render={data => (
    2. <h1>Hello {data.target}</h1>
    3. )}/>

    使用 render prop 的库有 React Router 和 Downshift.

    在这个文档中,我们将讨论为什么 render prop 是有用的,以及如何写一个自己的 render prop 组件。

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

    组件是 React 代码复用的主要单元,但如何分享一个组件封装到其他需要相同 state 组件的状态或行为并不总是很容易。

    例如,以下组件跟踪 Web 应用程序中的鼠标位置:

    1. class MouseTracker extends React.Component {
    2. constructor(props) {
    3. super(props);
    4. this.handleMouseMove = this.handleMouseMove.bind(this);
    5. this.state = { x: 0, y: 0 };
    6. }
    7. handleMouseMove(event) {
    8. this.setState({
    9. x: event.clientX,
    10. y: event.clientY
    11. });
    12. }
    13. render() {
    14. return (
    15. <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
    16. <h1>移动鼠标!</h1>
    17. <p>当前的鼠标位置是 ({this.state.x}, {this.state.y})</p>
    18. </div>
    19. );
    20. }
    21. }

    当光标在屏幕上移动时,组件在 <p> 中显示其(x,y)坐标。

    现在的问题是:我们如何在另一个组件中复用这个行为?换个说法,若另一个组件需要知道鼠标位置,我们能否封装这一行为,以便轻松地与其他组件共享它??

    由于组件是 React 中最基础的代码复用单元,现在尝试重构一部分代码使其能够在 <Mouse> 组件中封装我们需要共享的行为。

    1. // <Mouse> 组件封装了我们需要的行为...
    2. class Mouse extends React.Component {
    3. constructor(props) {
    4. super(props);
    5. this.handleMouseMove = this.handleMouseMove.bind(this);
    6. this.state = { x: 0, y: 0 };
    7. }
    8. handleMouseMove(event) {
    9. this.setState({
    10. x: event.clientX,
    11. y: event.clientY
    12. });
    13. }
    14. render() {
    15. return (
    16. <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
    17. {/* ...但我们如何渲染 <p> 以外的东西? */}
    18. <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
    19. </div>
    20. );
    21. }
    22. }
    23. class MouseTracker extends React.Component {
    24. render() {
    25. return (
    26. <div>
    27. <h1>移动鼠标!</h1>
    28. <Mouse />
    29. </div>
    30. );
    31. }
    32. }

    现在 <Mouse> 组件封装了所有关于监听 mousemove 事件和存储鼠标 (x, y) 位置的行为,但其仍不是真正的可复用。

    举个例子,假设我们有一个 组件,它可以呈现一张在屏幕上追逐鼠标的猫的图片。我们或许会使用 <Cat mouse={{ x, y }} prop 来告诉组件鼠标的坐标以让它知道图片应该在屏幕哪个位置。

    首先, 你或许会像这样,尝试在 <Mouse> 内部的渲染方法渲染 <Cat> 组件::

    1. class Cat extends React.Component {
    2. render() {
    3. const mouse = this.props.mouse;
    4. return (
    5. <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    6. );
    7. }
    8. }
    9. class MouseWithCat extends React.Component {
    10. constructor(props) {
    11. super(props);
    12. this.handleMouseMove = this.handleMouseMove.bind(this);
    13. this.state = { x: 0, y: 0 };
    14. }
    15. handleMouseMove(event) {
    16. this.setState({
    17. x: event.clientX,
    18. y: event.clientY
    19. });
    20. }
    21. render() {
    22. return (
    23. <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
    24. {/*
    25. 我们可以在这里换掉 <p> 的 <Cat> ......
    26. 但是接着我们需要创建一个单独的 <MouseWithSomethingElse>
    27. 每次我们需要使用它时,<MouseWithCat> 是不是真的可以重复使用.
    28. */}
    29. <Cat mouse={this.state} />
    30. </div>
    31. );
    32. }
    33. }
    34. class MouseTracker extends React.Component {
    35. render() {
    36. return (
    37. <div>
    38. <h1>移动鼠标!</h1>
    39. <MouseWithCat />
    40. </div>
    41. );
    42. }
    43. }

    这种方法适用于我们的特定用例,但我们还没有达到以可复用的方式真正封装行为的目标。现在,每当我们想要鼠标位置用于不同的用例时,我们必须创建一个新的组件(本质上是另一个 <MouseWithCat> ),它专门为该用例呈现一些东西.

    这也是 render prop 的来历:我们可以提供一个带有函数 prop 的 <Mouse> 组件,它能够动态决定什么需要渲染的,而不是将 <Cat> 硬编码到 <Mouse> 组件里,并有效地改变它的渲染结果。

    1. class Cat extends React.Component {
    2. render() {
    3. const mouse = this.props.mouse;
    4. return (
    5. <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    6. );
    7. }
    8. }
    9. class Mouse extends React.Component {
    10. constructor(props) {
    11. super(props);
    12. this.handleMouseMove = this.handleMouseMove.bind(this);
    13. this.state = { x: 0, y: 0 };
    14. }
    15. handleMouseMove(event) {
    16. this.setState({
    17. x: event.clientX,
    18. y: event.clientY
    19. });
    20. }
    21. render() {
    22. return (
    23. <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
    24. {/*
    25. Instead of providing a static representation of what <Mouse> renders,
    26. use the `render` prop to dynamically determine what to render.
    27. */}
    28. {this.props.render(this.state)}
    29. </div>
    30. );
    31. }
    32. }
    33. class MouseTracker extends React.Component {
    34. render() {
    35. return (
    36. <div>
    37. <h1>移动鼠标!</h1>
    38. <Mouse render={mouse => (
    39. <Cat mouse={mouse} />
    40. )}/>
    41. </div>
    42. );
    43. }
    44. }

    现在,我们提供了一个 render 方法 让 <Mouse> 能够动态决定什么需要渲染,而不是克隆 <Mouse> 组件然后硬编码来解决特定的用例。

    更具体地说,render prop 是一个用于告知组件需要渲染什么内容的函数 prop。

    这项技术使我们共享行为非常容易。要获得这个行为,只要渲染一个带有 render prop 的 <Mouse> 组件就能够告诉它当前鼠标坐标 (x, y) 要渲染什么。

    关于 render prop 一个有趣的事情是你可以使用带有 render prop 的常规组件来实现大多数高阶组件 (HOC)。 例如,如果你更喜欢使用 withMouse HOC而不是 <Mouse> 组件,你可以使用带有 render prop 的常规 <Mouse> 轻松创建一个:

    1. // 如果你出于某种原因真的想要 HOC,那么你可以轻松实现
    2. // 使用具有 render prop 的普通组件创建一个!
    3. function withMouse(Component) {
    4. return class extends React.Component {
    5. render() {
    6. return (
    7. <Mouse render={mouse => (
    8. <Component {...this.props} mouse={mouse} />
    9. )}/>
    10. );
    11. }
    12. }
    13. }

    因此,你可以将任一模式与 render prop 一起使用。

    使用 Props 而非 render

    重要的是要记住,render prop 是因为模式才被称为 render prop ,你不一定要用名为 render 的 prop 来使用这种模式。事实上, 任何被用于告知组件需要渲染什么内容的函数 prop 在技术上都可以被称为 “render prop”.

    尽管之前的例子使用了 render,我们也可以简单地使用 children prop!

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

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

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

    你将在 react-motion 的 API 中看到此技术。

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

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

    注意事项

    将 Render Props 与 React.PureComponent 一起使用时要小心

    如果你在 render 方法里创建函数,那么使用 render prop 会抵消使用 React.PureComponent 带来的优势。因为浅比较 props 的时候总会得到 false,并且在这种情况下每一个 render 对于 render prop 将会生成一个新的值。

    例如,继续我们之前使用的 <Mouse> 组件,如果 Mouse 继承自 React.PureComponent 而不是 React.Component,我们的例子看起来就像这样:

    class Mouse extends React.PureComponent {
      // 与上面相同的代码......
    }
    
    class MouseTracker extends React.Component {
      render() {
        return (
          <div>
            <h1>Move the mouse around!</h1>
    
            {/*
              这是不好的!
              每个渲染的 `render` prop的值将会是不同的。
            */}
            <Mouse render={mouse => (
              <Cat mouse={mouse} />
            )}/>
          </div>
        );
      }
    }

    在这样例子中,每次 <MouseTracker> 渲染,它会生成一个新的函数作为 <Mouse render> 的 prop,因而在同时也抵消了继承自 React.PureComponent<Mouse> 组件的效果!

    为了绕过这一问题,有时你可以定义一个 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),则 <Mouse> 应该扩展 React.Component