所谓Render Props,涉及一个使用值为函数得的props在React组件间共享代码的简单技术。

携带render props的组件,这个props是一个返回一个React元素的函数并且通过调用该函数来代替其自身的渲染逻辑。

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

使用了该项技术的类库有ReactRouter,Downshif,React Motion等。

这篇文章我们将会讨论为什么render props是有用的,以及怎样自己写一个。

Cross-Cutting Concerns 横切点来使用Render Props

React中,组件是基本复用单元。但是对于一个组件封装另一个需要相同state的组件来说,共享state或者行为并不总是显而易见。

比如,下面的代码是追踪鼠标位置的:

  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>Move the mouse around!</h1>
  17. <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
  18. </div>
  19. );
  20. }
  21. }

当鼠标在屏幕移动,组件将鼠标的x,y坐标展示在p标签中。

现在的问题是:在另一组件中,我们如何复用次行为?换句话,如果一个组件需要知道鼠标位置,我们能否将这个行为封装起来,以便我们能在组建中共享起来?

既然组件是React的基本单元,那么让我们一点点尝试重构代码:用一个Mouse组件封装我们想要的行为:

  1. // The <Mouse> component encapsulates the behavior we need...
  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. {/* ...but how do we render something other than a <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>Move the mouse around!</h1>
  28. <Mouse />
  29. </div>
  30. );
  31. }
  32. }

现在Mouse组件封装了所有因为监听mousemove事件而关联的行为,同时将(x, y)坐标存储起来。但这依旧不是真正的复用。

就比如,我们假象一个Cat组件,它在屏幕上渲染一个抓老鼠的小猫。我们期望能用《Cat mouse={{x, y}}》来告知组件老鼠的坐标,这样来决定将这张图片展示在什么地方。

一开始,你可能想将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 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. We could just swap out the <p> for a <Cat> here ... but then
  26. we would need to create a separate <MouseWithSomethingElse>
  27. component every time we need to use it, so <MouseWithCat>
  28. isn't really reusable yet.
  29. */}
  30. <Cat mouse={this.state} />
  31. </div>
  32. );
  33. }
  34. }
  35. class MouseTracker extends React.Component {
  36. render() {
  37. return (
  38. <div>
  39. <h1>Move the mouse around!</h1>
  40. <MouseWithCat />
  41. </div>
  42. );
  43. }
  44. }

这个方法对我们这个特殊的需求的确奏效。但是我们还没有达到真正以可重用的方式封装的目的。现在是,每次想要使用鼠标位置,我们都得创建一个新的满足我们特殊要求的组件(基本上是另一个MouseWithCat)。

Render Props技术这就来了:我们将给Mouse组件中传递一个函数的Props,该函数用来动态决定怎样渲染Render Props,这样来取代在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 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>Move the mouse around!</h1>
  38. <Mouse render={mouse => (
  39. <Cat mouse={mouse} />
  40. )}/>
  41. </div>
  42. );
  43. }
  44. }

现在,取代实际等于拷贝Mouse组件以及硬编码在render方法来解决这个特殊用例,我们提供名字是render的属性,这样Mouse组件可以动态的决定要渲染什么。

更正确的说,名为render的props是一个可以让组件知道要自己要渲染的东西。

这项技术使得我们共享的需求十分简单。为了达成目的,我们渲染Mouse时要用render props来告知当前鼠标的x,y坐标。

另一个有趣的事情是,大多数的高阶组件HOC可以用上这种常规组件携带render props的方法。比如说, 想要一个withMouse的HOC来替代Mouse组件,你可以简单的创造一个常规Mouse组件带上一个render props。

  1. // If you really want a HOC for some reason, you can easily
  2. // create one using a regular component with a 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的props让两种模式都得以实现。

使用Props不仅仅是render

记住这个很重要,这个模式叫“render props”,所以你没必要必须用一个名为render的 prop来使用该模式。事实上,任何一个函数props,其作用是提供组件的渲染元素,都叫render props。

尽管上面的例子都在使用render。但是我们可以简单的使用children属性。

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

同时要记住,children属性实际不需要写在JSX的一串属性中。你可以将它直接写在组件内部。

  1. <Mouse>
  2. {mouse => (
  3. <p>The mouse position is {mouse.x}, {mouse.y}</p>
  4. )}
  5. </Mouse>

你在react-motion库中常见此技术。

既然该技术有点不寻常,你可能想在设计API的时候在propTypes中,明确的声明children应该是一个function。

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

警告

在React.PureComponent中小心使用名为Render的Props

当你在render中新建一个函数,那么使用这个技术就抵消了使用React.PureComponent的优点。这是因为钱比较对于新的props来讲会返回false(不相等),而每一次渲染都会造成一个新的render props产生(箭头函数对象)。

比如,继续我们上面提到的例子Mouse组件,如果Mouse组件集成了PureComponent,它就是这样的:

  1. class Mouse extends React.PureComponent {
  2. // Same implementation as above...
  3. }
  4. class MouseTracker extends React.Component {
  5. render() {
  6. return (
  7. <div>
  8. <h1>Move the mouse around!</h1>
  9. {/*
  10. This is bad! The value of the `render` prop will
  11. be different on each render.
  12. */}
  13. <Mouse render={mouse => (
  14. <Cat mouse={mouse} />
  15. )}/>
  16. </div>
  17. );
  18. }
  19. }

这个例子中,每次MouseTracker渲染后,就产生新的函数对象作为Mouse的属性,这样就是一开始就抵消了Mouse集成PureComponent的作用。

绕过这个问题的办法,就是你可以定义属性的值为一个实例方法,像这样:

  1. class MouseTracker extends React.Component {
  2. // Defined as an instance method, `this.renderTheCat` always
  3. // refers to *same* function when we use it in render
  4. renderTheCat(mouse) {
  5. return <Cat mouse={mouse} />;
  6. }
  7. render() {
  8. return (
  9. <div>
  10. <h1>Move the mouse around!</h1>
  11. <Mouse render={this.renderTheCat} />
  12. </div>
  13. );
  14. }
  15. }

那些你不能定义静态属性的例子中(比如你要关闭组件的属性后状态?),Mouse应该集成React。Component。

官网文章 Advanced Guides :Render Props