【译】在 React 中拥抱函数——无状态函数式组件及其重要性
原文:Embracing Functions in React - Stateless Functional Components and Why they Matter
说明:因个人水平有限,如有误译,欢迎指正。
介绍
本篇文章不是向您介绍任何最佳实践或者使用 React 编写应用的某个“唯一方式”。
本文讲述的都是关于 React 中的无状态函数式组件 (stateless functional component) 以及为什么它们可能有用或为什么应首先受到考虑。
定义
在我们探讨这个问题之前,我们先了解一下在 React 上下文中函数式组件的定义。它本质上就是一个常规的函数,接收一个 props 并返回一个元素。
function Item(props) {
return (
<div className='item'>
{props.title}
<span
className='deleteItem'
onClick={props.remove(props.id)}
> x </span>
</div>
)
}
使用 ES6 的箭头函数和解构,我们也可以这样编写:
const Item = ({ id, title, remove }) => <div className='item'>
{title}
<span
className='deleteItem'
onClick={remove(id)}
> x </span>
</div>
初看,这与使用 createClass 和 ES6 Class 并没有什么特别之处,无状态函数式组件仅仅是一种编写风格。但是从我的观点来看却有很多不同。
现在让我们看一下函数式组件和基于类的方式的不同之处,并根据给定的事实,我们能推导出什么有价值的内容。
无生命周期方法
函数式组件,有时也被称为无状态组件,没有任何生命周期方法,意味着每次上层组件树状态发生变更时它们都会重新渲染,这就是因为缺少 shouldComponentUpdate
方法导致的。这也同样意味着您不能定义某些基于组件挂载和卸载的行为。
没有 this 和 ref
更有趣的是您在函数式组件中既不能使用 this 关键字或访问到 ref。对于习惯了严格意义上的类或面向对象风格的人来说,这很让他们惊讶。这也是使用函数最大的争论点。
另一个有趣的事实就是您仍然可以访问到 context,如果您将 context 定义为函数的一个 props。
为何甚至将函数认为实际可行的方式
所以您可能会问自己,它的优势究竟体现在哪里。特别是您已经使用了 React 并倾向于使用基于类的方式。当我们拥抱这个概念的时候,容器型组件 (container component) 和 展示型组件 (presentational component) 的概念就会变得非常清晰。您也可以阅读 Dan Abramov 关于此主题的 文章 以获取更深的了解。
通过将逻辑和数据处理与 UI 展示剥离开来,我们就可以避免在展示型组件中处理任何的状态。无状态函数式组件强制实施了这样的风格,因为您无论如何都没有办法处理本地状态了。它强制您将任何的状态处理移交至上层的组件树,而让下层的组件只做它们所做的——关注 UI 的展示。
没有逻辑意味着相同的表示具有相同的数据。
避免常见陷阱
在编写无状态函数式组件时,您需要避免某些特定的模式。避免在函数式组件中定义函数,这是因为每一次函数式组件被调用的时候,一个新的函数都会被创建。
const Form = ({...}) => {
const handleSomething = e => path(['event', 'target'], e)
return (
// ...
)
}
这个问题很容易解决,您可以将这个函数作为 props 传递进去,或者将它定义在组件外面。
const handleSomething = e => path(['event', 'target'], e)
const Form = ({...}) => // ...
有时候谈起无状态函数式组件会提到纯 (pure) 这个词。在这方面您应该避免使用 context 或 defaultProps,如果您需要定义上述任何一个或两个,您应该选择基于类的方式。
const ListComponent = ({...})
ListComponent.contextTypes = {
style: React.PropTypes.object.isRequired
}
ListComponent.defaultProps = {
items: []
}
至于 defaultProps,一个变通的方案就是使用默认参数。
const ListComponent = ({ items = [] }) => (...)
关于纯函数,请查看 Bernhard Gschwantner 的 评论,他总结得非常完美。
另一个常见陷阱就是简单地认为使用纯无状态函数式组件可以获得性能上的提升。这个观点是不正确的。相反,当我们需要处理大量无状态函数型组件的时候,它的对立观点却是正确的。
性能问题是由于缺少生命周期方法导致的,这就意味着 React 没有访问任何额外的方法并且总是渲染组件。
缺少声明周期方法在另一方面也导致了没法定义 shouldComponentUpdate 方法。因此,我们不能够告诉 React 是重新渲染还是不渲染,这也就导致了永远都会重新渲染。接下来我们将会了解到缓解这种问题的方法。
高阶组件
如果您想了解更多高阶组件以及使用它们的优点,请查看 Why The Hipsters Recompose Everything。
高阶组件是一种接收组件为参数并返回一个新的组件的函数。
HOC :: Component -> Component
这种方式可以让我们解决许多因使用无状态函数式组件而导致的问题。简言之,我们可以将函数式组件封装进高阶组件以解决状态处理和渲染优化这样的问题,高阶组件可以帮助我们关注本地状态处理以及 shouldComponentUpdate 函数的实现。
Recompose 就帮我们解决了以上提到的情况。
下面的这个例子是直接从这个项目 README 文件中拿来的。
// 渲染代价较高的组件
const ExpensiveComponent = ({ propA, propB }) => {...}
// 与 React's PureRenderMixin 作用相同
const OptimizedComponent = pure(ExpensiveComponent)
// 继续优化:仅在特定 prop 键值发生变化才更新
const HyperOptimizedComponent =
onlyUpdateForKeys(['propA', 'propB'])(ExpensiveComponent)
正如上面所示,我们可以将精力集中于 UI 表示并在需要的时候将函数封装进一个纯函数并导出这个封装的函数。我们就不需要将原始的函数重构为一个类组件。
下面的示例来自于 Why The Hipsters Recompose Everything。
const withState = (stateName, stateUpdateFn, initialState) => {
return Comp => {
const factory = createFactory(Comp)
return createClass({
getInitialState() { return { value: initialState } },
stateUpdateFn(fn) {
this.setState(({ value } ) => ({ value: fn(value) }))
},
render() {
return factory({
...this.props,
[stateName] : this.state.value,
[stateUpdateFn] : this.stateUpdateFn,
})
}
})
}
}
withState 使得我们在有需要的时候管理本地组件状态,只需将我们的无状态函数式组件传递给 enhance 函数即可。
const enhance = withState('counter', 'setCounter', 0)
const Counter = enhance(({ counter, setCounter }) =>
<div>
Count: {counter}
<button onClick={() => setCounter(n => n+1)}>Increment</button>
<button onClick={() => setCounter(n => n-1)}>Decrement</button>
</div>
)
同样,recompose 已经实现了 withState,所以就没有必要自己再去实现它了。
结束
使用无状态函数式组件最大的好处就是它能够将容器型和展示型组件明确区分开来,避免产生大型以及杂乱的组件。没有 this
关键字也就意味着没有快捷方式在整个应用中随机地展开状态。
当一个开发团队中人员经验存在差别时,这些方面就会变得异常有用,它会帮助我们间接地执行内部开发的标准。