本文作者:IMWeb nixzheng 原文出处:IMWeb社区 未经同意,禁止转载
React让很多人让追捧的一个特性是它的所有的组件都是完全由JavaScript组成的。组件的定义是JavaScript,组件的模板也可以是JavaScript,组件的样式也可以是JavaScript(参考styled-component)。React并没有创造太多概念,唯一的创造品——JSX,其内部的statement也是一段段纯JavaScript代码,并且在Babel编译后依然转变成了JavaScript。
而JavaScript又是一个把函数当作一等公民的语言。函数不仅可以被声明和调用,也可以像值一样做赋值、传参、返回的操作。
这样运行在一个有着first-class functions特性的语言之上的纯JavaScript组件库,自然可以脑洞大开的有很多玩法。
Stateless Component
使用React的同学自然对这个概念一点都陌生。虽然大多数情况下我们都会使用 class extends React.Component
来声明一个Stateful Component,虽然Stateless Component没有完整的生命周期,虽然Stateless Component的性能相比Stateful Component并没有提升,但是它在很多场合下仍然是有意义的。下面是一个最简单的Stateless Component的声明:
function Welcome({name}) {
return <div>{name}</div>
}
由于这是一个纯函数,我们可以基于它创建一个简单的High Order Component。
代码语言:javascript复制const withNameX = WrappedComponent => props => WrappedComponent(Object.assign({}, props, {name: ‘x’}))
const WelcomeX = withNameX(Welcome)
我们也可以对 Welcome
做简单的 compose。
const toUpperCase = props => Object.keys(props).reduce((target, next) => {
target[next] = props[next].toUpperCase()
return target
})
const UpperWelcome = compose(Welcome, toUpperCase )
使用Stateless Component好处很多,包括
- Pure。单元测试很方便。
- 强制你从更简单的角度思考组件的组织。单个函数的代码量更小,功能更单一。「The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that.」——《Clean Code》
- 可以使用compose、curry等特性构件可复用组件。
Stateless Component最大的不足是它没有能力在最佳实践的前提下处理需要回传属性的事件回调,我们只能写成这样:
代码语言:javascript复制const Trigger = ({onClick, id}) => <Button onClick={() => onClick(id)}>test</Button>
由于每次调用都会生成一个新的匿名函数,子组件是无法利用PureComponent做优化的。这是我在实际工作场景下使用Stateless Component最大的障碍。
recompose
上面的障碍当然也是有解的,recompose是一个为Stateless React爱好者提供的一个工具库。我们可以使用它提供的 withHandlers
方法。
const Trigger = withHandlers({
onClick: props => () => props.onClick(props.id)
})(({onClick}) => <Button onClick={onClick}>test</Button>)
是不是也很优雅。
当然为了能处理这种类型的回调,withHandlers
内部也是使用了Stateful Component的,感兴趣的同学可以看看recompose的源码。
recompose还有 withState
, pure
, onlyUpdateForKeys
, withContext
等很多实用的工具函数,帮助我们至少从代码编写角度实现全面使用Stateless Component替代Stateful Component。
Function as child Components
这也是React社区一种常见的组件构建方式。它也能解决HOC中丢失上下文、丢失ref的问题。它也能有效的提升代码复用率,而且某些情况下比HOC要更加优雅。
一个最简单的Function as Child Component如下:
代码语言:javascript复制class MyComponent extends React.Component {
render() {
return (
<div>
{this.props.children('world')}
</div>
);
}
}
<MyComponent>
{(name) => (
<div>Hello {name}!</div>
)}
</MyComponent>
PayPal开源的downshift就是使用Function as Child Component模型来构建他们的autocomplete,dropdown, select等组件的。
一般我们写一个autocomplete组件,是基于Popover -> Menu InputTrigger -> AutoComplete这样逐步组合、增强基础组件的方式。这么写会有几个问题:高级组件或者完全无法获取底层组件的引用,或者需要通过很奇怪的方式把引用回调一层层传下去;为了适配很多情况和需求,为了能控制各组合组件的行为,高级组件的参数会多的可怕:ant.design的AutoComplete组件有14个参数,material-ui则有27个参数。
Downshift则完全不处理组件的展示和组合,这部分逻辑交给开发者自己,通过Function as Child Components的方式自由设计他们希望的样式和行为。Downshift只处理这一类组件的交互逻辑,维护组件状态,并暴露少数几个必须设置的子组件属性的接口。这样的代码组织,输入输出都很明确,组件间的耦合也很小,不仅解决了参数爆炸的问题,也提升了可维护性。
对比High Order Component与Function as Child Components
HOC | FaCC |
---|---|
使用者无关,HOC帮你完成了一切组件行为 | 使用者完全大部分组件展示和行为,更可控 |
HOC在运行时无法获取组件相关的state和props | 可以在运行时获取组件的 state & props |
HOC可以通过shouldComponentUpdate做优化 | FaCC由于每次render都会改变,无法使用shouldComponentUpdate做优化 |
总结
本文提到了两种组件设计的思路——利用recompose拆分组件为Stateless Component,使用Function as Child Components来剥离行为和展现——以此来提升代码的可读性和可维护性。实际项目中可以按需使用。
参考资料
- https://github.com/acdlite/recompose
- https://medium.com/merrickchristensen/function-as-child-components-5f3920a9ace9
- http://rea.tech/functions-as-child-components-and-higher-order-components/
- https://reactarmory.com/answers/how-should-i-separate-components