正文
更新 :我提交了一个 PR 到 React 官方文档,为其添加了 Render props 。
更新2 :添加一部分内容来说明 “children 作为一个函数” 也是相同的概念,只是 prop 名称不同罢了。
几个月前,我发了一个 twitter:
译注:@reactjs 我可以在一个普通组件上使用一个 render prop 来完成 HOC(高阶组件) 能够做到的事情。不服来辩。
我认为,高阶组件模式 作为一个在许多基于 React 的代码中流行的代码复用手段,是可以被一个具有 “render prop” 的普通组件 100% 地替代的。“不服来辩” 一词是我对 React 社区朋友们的友好 “嘲讽”,随之而来的是一个系列好的讨论,但最终,我对我自己无法用 140 字来完整描述我想说的而感到失望。 我 决定在未来的某个时间点写一篇更长的文章 来公平公正的探讨这个主题。
两周前,当 Tyler 邀请我到 Phoenix ReactJS 演讲时,我认为是时候去对此进行更进一步的探讨了。那周我已经到达 Phoenix 去启动 我们的 React 基础和进阶补习课 了,而且我还从我的商业伙伴 Ryan 听到了关于大会的好消息,他在四月份做了演讲 。
在大会上,我的演讲似乎有点标题党的嫌疑:不要再写另一个 HOC 了 。你可以在 Phoenix ReactJS 的 YouTube 官方频道 上观看我的演讲,也可以通过下面这个内嵌的视频进行观看:
如果你不想看视频的话,可以阅读后文对于演讲主要内容的介绍。但是严肃地说:视频要有趣多了 😀。
如果你直接跳过视频开始阅读,但并没有领会我所说的意思,就折回去看视频 吧。演讲时的细节会更丰富。
Mixins 存在的问题
我的演讲始于高阶组件主要解决的问题:代码复用 。
让我们回到 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'
const MouseMixin = {
getInitialState() {
return { x : 0 , y : 0 }
},
handleMouseMove(event) {
this .setState({
x : event.clientX,
y : event.clientY
})
}
}
const App = React.createClass({
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
属性获得鼠标的 x
和 y
坐标。
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'
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 给予的动态组合模型的好处,那是特别酷的特性。😎
Michael 是 React Training 的成员,也是 React 社区中一个多产的开源软件贡献者 。想了解最新的培训和课程就[订阅邮件推送](subscribe to the mailing list) 并 在 Twitter 上关注 React Training 。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android 、iOS 、React 、前端 、后端 、产品 、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划 、官方微博 、知乎专栏 。