[译] 使用 Render props 吧!

更新我提交了一个 PR 到 React 官方文档,为其添加了 Render propshtml

更新2:添加一部份内容来讲明 “children 做为一个函数” 也是相同的概念,只是 prop 名称不一样罢了。前端


几个月前,我发了一个 twitter:react

译注:@reactjs 我能够在一个普通组件上使用一个 render prop 来完成 HOC(高阶组件) 可以作到的事情。不服来辩。android

我认为,高阶组件模式 做为一个在许多基于 React 的代码中流行的代码复用手段,是能够被一个具备 “render prop” 的普通组件 100% 地替代的。“不服来辩” 一词是我对 React 社区朋友们的友好 “嘲讽”,随之而来的是一个系列好的讨论,但最终,我对我本身没法用 140 字来完整描述我想说的而感到失望。 我 决定在将来的某个时间点写一篇更长的文章 来公平公正的探讨这个主题。ios

两周前,当 Tyler 邀请我到 Phoenix ReactJS 演讲时,我认为是时候去对此进行更进一步的探讨了。那周我已经到达 Phoenix 去启动 咱们的 React 基础和进阶补习课 了,并且我还从个人商业伙伴 Ryan 听到了关于大会的好消息,他在四月份作了演讲git

在大会上,个人演讲彷佛有点标题党的嫌疑:不要再写另外一个 HOC 了。你能够在 Phoenix ReactJS 的 YouTube 官方频道 上观看个人演讲,也能够经过下面这个内嵌的视频进行观看:github

若是你不想看视频的话,能够阅读后文对于演讲主要内容的介绍。可是严肃地说:视频要有趣多了 😀。typescript

若是你直接跳过视频开始阅读,但并无领会我所说的意思,就折回去看视频吧。演讲时的细节会更丰富。后端

Mixins 存在的问题

个人演讲始于高阶组件主要解决的问题:代码复用api

让咱们回到 2015 年使用 React.createClass 那会儿。假定你如今有一个简单的 React 应用须要跟踪并在页面上实时显示鼠标位置。你可能会构建一个下面这样的例子:

import React from 'react'
import ReactDOM from 'react-dom'

const App = React.createClass({
  getInitialState() {
    return { x: 0, y: 0 }
  },

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    })
  },

  render() {
    const { x, y } = this.state

    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> <h1>The mouse position is ({x}, {y})</h1> </div>
    )
  }
})

ReactDOM.render(<App/>, document.getElementById('app'))
复制代码

如今,假定咱们在另外一个组件中也须要跟踪鼠标位置。咱们能够重用 <App> 中的代码吗?

createClass 这个范式中,代码重用问题是经过被称为 “mixins” 的技术解决的。咱们建立一个 MouseMixin,让任何人都能经过它来追踪鼠标位置。

import React from 'react'
import ReactDOM from 'react-dom'

// mixin 中含有了你须要在任何应用中追踪鼠标位置的样板代码。
// 咱们能够将样板代码放入到一个 mixin 中,这样其余组件就能共享这些代码
const MouseMixin = {
  getInitialState() {
    return { x: 0, y: 0 }
  },

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    })
  }
}

const App = React.createClass({
  // 使用 mixin!
  mixins: [ MouseMixin ],
  
  render() {
    const { x, y } = this.state

    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> <h1>The mouse position is ({x}, {y})</h1> </div>
    )
  }
})

ReactDOM.render(<App/>, document.getElementById('app'))
复制代码

问题解决了,对吧?如今,任何人都能轻松地将 MouseMixin 混入他们的组件中,并经过 this.state 属性得到鼠标的 xy 坐标。

HOC 是新的 Mixin

去年,随着ES6 class 的到来,React 团队最终决定使用 ES6 class 来代替 createClass。这是一个明智的决定,没有人会在 JavaScript 都内置了 class 时还会维护本身的类模型。

但就存在一个问题:ES6 class 不支持 mixin。除了不是 ES6 规范的一部分,Dan 已经在一篇 React 博客上发布的博文上详细讨论了 mixin 存在的其余问题。

minxins 的问题总结下来就是

  • ES6 class。其不支持 mixins。
  • 不够直接。minxins 改变了 state,所以也就很难知道一些 state 是从哪里来的,尤为是当不止存在一个 mixins 时。
  • 名字冲突。两个要更新同一段 state 的 mixins 可能会相互覆盖。createClass API 会对两个 mixins 的 getInitialState 是否具备相同的 key 作检查,若是具备,则会发出警告,但该手段并不牢靠。

因此,为了替代 mixin,React 社区中的很多开发者最终决定用高阶组件(简称 HOC)来作代码复用。在这个范式下,代码经过一个相似于 装饰器(decorator) 的技术进行共享。首先,你的一个组件定义了大量须要被渲染的标记,以后用若干具备你想用共享的行为的组件包裹它。所以,你如今是在 装饰 你的组件,而不是混入你须要的行为!

import React from 'react'
import ReactDOM from 'react-dom'

const withMouse = (Component) => {
  return class extends React.Component {
    state = { x: 0, y: 0 }

    handleMouseMove = (event) => {
      this.setState({
        x: event.clientX,
        y: event.clientY
      })
    }

    render() {
      return (
        <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> <Component {...this.props} mouse={this.state}/> </div> ) } } } const App = React.createClass({ render() { // 如今,咱们获得了一个鼠标位置的 prop,而再也不须要维护本身的 state const { x, y } = this.props.mouse return ( <div style={{ height: '100%' }}> <h1>The mouse position is ({x}, {y})</h1> </div> ) } }) // 主须要用 withMouse 包裹组件,它就能得到 mouse prop const AppWithMouse = withMouse(App) ReactDOM.render(<AppWithMouse/>, document.getElementById('app')) 复制代码

让咱们和 mixin 说再见,去拥抱 HOC 吧。

在 ES6 class 的新时代下,HOC 的确是一个可以优雅地解决代码重用问题方案,社区也已经普遍采用它了。

此刻,我想问一句:是什么驱使咱们迁移到 HOC ? 咱们是否解决了在使用 mixin 时遇到的问题?

让咱们看下:

  • ES6 class。这里再也不是问题了,ES6 class 建立的组件可以和 HOC 结合。
  • 不够直接。即使用了 HOC,这个问题仍然存在。在 mixin 中,咱们不知道 state 从何而来,在 HOC 中,咱们不知道 props 从何而来。
  • 名字冲突。咱们仍然会面临该问题。两个使用了同名 prop 的 HOC 将遭遇冲突而且彼此覆盖,而且此次问题会更加隐晦,由于 React 不会在 prop 重名是发出警告。

另外一个 HOC 和 mixin 都有的问题就是,两者使用的是 静态组合 而不是 动态组合。问问你本身:在 HOC 这个范式下,组合是在哪里发生的?当组件类(如上例中的的 AppWithMouse)被建立后,发生了一次静态组合。

你没法在 render 方法中使用 mixin 或者 HOC,而这恰是 React 动态 组合模型的关键。当你在 render 中完成了组合,你就能够利用到全部 React 生命期的优点了。动态组合或许微不足道,但兴许某天也会出现一篇专门探讨它的博客,等等,我有点离题了。😅

总而言之:使用 ES6 class 建立的 HOC 仍然会遇到和使用 createClass 时同样的问题,它只能算一次重构。

如今不要说拥抱 HOC 了,咱们不过在拥抱新的 mixin!🤗

除了上述缺陷,因为 HOC 的实质是包裹组件并建立了一个混入现有组件的 mixin 替代,所以,HOC 将引入大量的繁文缛节。从 HOC 中返回的组件须要表现得和它包裹的组件尽量同样(它须要和包裹组件接收同样的 props 等等)。这一事实使得构建健壮的 HOC 须要大量的样板代码(boilerplate code)。

上面我所讲到的,以 React Router 中的 withRouter HOC 为例,你能够看到 props 传递wrappedComponentRef被包裹组件的静态属性提高(hoist)等等这样的样板代码,当你须要为你的 React 添加 HOC 时,就不得不撰写它们。

Render Props

如今,有了另一门技术来作代码复用,该技术能够规避 mixin 和 HOC 的问题。在 React Training 中,称之为 “Render Props”。

我第一次见到 render prop 是在 ChengLou 在 React Europe 上 关于 react-motion 的演讲,大会上,他提到的 <Motion children> API 能让组件与它的父组件共享 interpolated animation。若是让我来定义 render prop,我会这么定义:

一个 render prop 是一个类型为函数的 prop,它让组件知道该渲染什么。

更通俗的说法是:不一样于经过 “混入” 或者装饰来共享组件行为,一个普通组件只须要一个函数 prop 就可以进行一些 state 共享

继续到上面的例子,咱们将经过一个类型为函数的 render 的 prop 来简化 withMouse HOC 到一个普通的 <Mouse> 组件。而后,在 <Mouse>render 方法中,咱们可使用一个 render prop 来让组件知道如何渲染:

import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'

// 与 HOC 不一样,咱们可使用具备 render prop 的普通组件来共享代码
class Mouse extends React.Component {
  static propTypes = {
    render: PropTypes.func.isRequired
  }

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

const App = React.createClass({
  render() {
    return (
      <div style={{ height: '100%' }}> <Mouse render={({ x, y }) => ( // render prop 给了咱们所须要的 state 来渲染咱们想要的 <h1>The mouse position is ({x}, {y})</h1> )}/> </div> ) } }) ReactDOM.render(<App/>, document.getElementById('app')) 复制代码

这里须要明确的概念是,<Mouse> 组件其实是调用了它的 render 方法来将它的 state 暴露给 <App> 组件。所以,<App> 能够随便按本身的想法使用这个 state,这太美妙了。😎

在此,我想说明,“children as a function” 是一个 彻底相同的概念,只是用 children prop 替代了 render prop。我挂在嘴边的 render prop 并非在强调一个 名叫 prop 的 prop,而是在强调你使用一个 prop 去进行渲染的概念。

该技术规避了全部 mixin 和 HOC 会面对的问题:

  • ES6 class。不成问题,咱们能够在 ES6 class 建立的组件中使用 render prop。
  • 不够直接。咱们没必要再担忧 state 或者 props 来自哪里。咱们能够看到经过 render prop 的参数列表看到有哪些 state 或者 props 可供使用。
  • 名字冲突。如今不会有任何的自动属性名称合并,所以,名字冲突将全无可乘之机。

而且,render prop 也不会引入 任何繁文缛节,由于你不会 包裹装饰 其余的组件。它仅仅是一个函数!若是你使用了 TypeScript 或者 Flow,你会发现相较于 HOC,如今很容易为你具备 render prop 的组件写一个类型定义。固然,这是另一个话题了。

另外,这里的组合模型是 动态的!每次组合都发生在 render 内部,所以,咱们就能利用到 React 生命周期以及天然流动的 props 和 state 带来的优点。

使用这个模式,你能够将 任何 HOC 替换一个具备 render prop 的通常组件。这点咱们能够证实!😅

Render Props > HOCs

一个更将强有力的,可以证实 render prop 比 HOC 要强大的证据是,任何 HOC 都能使用 render prop 替代,反之则否则。下面的代码展现了使用一个通常的、具备 render prop 的 <Mouse> 组件来实现的 withMouse HOC:

const withMouse = (Component) => {
  return class extends React.Component {
    render() {
      return <Mouse render={mouse => (
        <Component {...this.props} mouse={mouse}/>
      )}/>
    }
  }
}
复制代码

有心的读者可能已经意识到了 withRouter HOC 在 React Router 代码库中确实就是经过**一个 render prop ** 实现的!

因此还不心动?快去你本身的代码中使用 render prop 吧!尝试使用具备 render prop 组件来替换 HOC。当你这么作了以后,你将再也不受困于 HOC 的繁文缛节,而且你也将利用到 React 给予的动态组合模型的好处,那是特别酷的特性。😎

MichaelReact Training 的成员,也是 React 社区中一个多产的开源软件贡献者。想了解最新的培训和课程就[订阅邮件推送](subscribe to the mailing list) 并 在 Twitter 上关注 React Training


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索