React进阶系列: Render Props 从介绍到实践

1,227 阅读5分钟

什么是render props

render props是一个组件间共享代码逻辑的小技巧, 通过props传递函数来实现。有许多库(比如React Router, React Motion)都使用了这个技巧。

组件有一个叫做renderprop, 值是一个返回React元素的函数, 在组件内部调用这个函数渲染组件。

语言描述不够直观, 来一个例子:

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

有什么用?

从一个例子开始, 假设我们需要一个追踪鼠标位置的组件:

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>
    );
  }
}

这个组件能跟随着鼠标移动显示坐标, 现在问题是: 我们怎么将这个逻辑复用到其他组件中?比如我们需要一个跟随鼠标位置显示图片的组件。换句话说: 我们怎么将追踪鼠标位置封装为可供其他组件复用的逻辑?

高阶组件(HOC)似乎是一个办法:

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}>

        {/*
          我们可以把希望渲染的东西放在这里
          但是如果希望实现其他功能
          每次都需要创建一个<MouseWithSomethingElse>的新组件
          所以这不是真正的复用
        */}
        <Cat mouse={this.state} />
      </div>
    );
  }
}

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

使用这种方法在特定用途下才有用, 并没有真正的将功能封装成可复用的逻辑, 每次我们需要一个新的追踪鼠标位置的用例, 都要创建一个新的<MouseWithSomethingElse>组件, 而这些组件间的追踪鼠标位置的代码是冗余的。

这个时候Render Props就登场了:

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}>

        {/*
          使用传入函数的逻辑动态渲染
          而不是硬编码地渲染固定内容
        */}
        {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>
    );
  }
}

现在我们可以随处复用追踪鼠标的逻辑而不用写重复的代码或者创建特定用途的组件了, 这才是真正的逻辑复用。

使用其他props

虽然这个技巧或者说模式(Pattern)叫Render Props, 但是并不一定要使用render来传递渲染函数, 你甚至可以使用children:

<Mouse>
  {mouse => (
    <p>The mouse position is {mouse.x}, {mouse.y}</p>
  )}
</Mouse>

这正是React Motion库使用的API。

(了解过另一个React模式 - Render callback的朋友会发现这时候就跟这个模式一样了。)

使用中的注意事项

当与React.PureComponent一起使用时要注意

对React熟悉的朋友都知道(不熟悉的可以看这篇文章), 当props或者state变化时, React默认都会重新渲染, 有两种方法来避免不必要的重复渲染:

  1. 手动实现shouldComponentUpdate方法
  2. 继承React.PureComponent, 这时会自动进行浅比较

但是当你使用Render Props时, 每次传入的render都是一个新的函数, 所以每次浅比较都会导致重新渲染。

为了避免这个问题, 你可以将prop定义为一个实例方法:

class MouseTracker extends React.Component {
  constructor(props) {
    super(props);

    // 在这里绑定来避免每次传入一个新的实例
    this.renderTheCat = this.renderTheCat.bind(this);
  }

  renderTheCat(mouse) {
    return <Cat mouse={mouse} />;
  }

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

实践

介绍了用法用途和注意事项之后, 我们来看在实际项目中如何使用。

一个很好的例子是动画效果, 因为绝大多数动效都是可以复用的, 并且动画效果要渲染的内容需要根据实际使用场景变化, 很难复用, react-motion这个库很好的实现了一套覆盖大多数使用场景的复用方法。

react-motion使用起来非常简单直观, 由库来提供动画效果的变化, 用户来编写实际需要渲染的内容。

import {Motion, spring} from 'react-motion';

<Motion style={{x: spring(this.state.open ? 400 : 0)}}>
  {({x}) =>
    <div className="demo0">
      <div className="demo0-block" style={{
        WebkitTransform: `translate3d(${x}px, 0, 0)`,
        transform: `translate3d(${x}px, 0, 0)`,
      }} />
    </div>
  }
</Motion>

react motion源码中的关键部分:

render(): ReactElement {
  // 调用通过children传入的渲染函数, 将this.state.currentStyle作为参数传入
  const renderedChildren = this.props.children(this.state.currentStyle);
  return renderedChildren && React.Children.only(renderedChildren);
}

componentDidMount() {
  this.prevTime = defaultNow();
  // 组件挂载后运行关键的动画效果函数
  // 动画效果函数封装的是可复用动画处理逻辑
  // 根据时间变化动态地对this.state.currentStyle更新
  this.startAnimationIfNecessary();
}

react motion将动画效果从每个组件硬编码的逻辑抽离出来并封装成可复用的库, 库仅负责对动画变换过程中的逻辑运算, 实际的渲染由用户定义, 用户根据库计算出来的数值来渲染, 在覆盖大多数使用场景的同时实现了最大程度的复用行。

总结

Render Props是一种复用通用逻辑的模式, 在实际开发中应该根据实际场景选择合适的方法/模式(不管是HOC还是Render Props), 最大程度地利用React的复用性, 保持代码DRY。

Render Props已经出现在了React官方文档的Advance Guide中, 并且有许多的开源库(点击这里查看)使用它来设计API实现。

参考

React官方文档
Github - react-motion