以前分享过几篇关于React技术栈的文章:html
一个react+redux工程实例github
......算法
今天再来同你们讨论 React 组件设计的一个有趣话题:分解 React 组件的几种进阶方法。编程
React 组件魔力无穷,同时灵活性超强。咱们能够在组件的设计上,玩转出不少花样。可是保证组件的Single responsibility principle: 单一原则很是重要,它可使得咱们的组件更简单、更方便维护,更重要的是使得组件更加具备复用性。redux
可是,如何对一个功能复杂且臃肿的 React 组件进行分解,也许并非一件简单的事情。本文由浅入深,介绍三个分解 React 组件的方法。架构
这是一个最容易想到的方法:当一个组件渲染了不少元素时,就须要尝试分离这些元素的渲染逻辑。最迅速的方式就是切割 render() 方法为多个 sub-render 方法。
看下面的例子会更加直观:
class Panel extends React.Component { renderHeading() { // ... } renderBody() { // ... } render() { return ( <div> {this.renderHeading()} {this.renderBody()} </div> ); }
细心的读者很快就能发现,其实这并无分解组件自己,该 Panel 组件仍然保持有原先的 state, props, 以及 class methods。
如何真正地作到减小复杂度呢?咱们须要建立一些子组件。此时,采用最新版 React 支持并推荐的函数式组件/无状态组件必定会是一个很好的尝试:
const PanelHeader = (props) => ( // ... ); const PanelBody = (props) => ( // ... ); class Panel extends React.Component { render() { return ( <div> // Nice and explicit about which props are used <PanelHeader title={this.props.title}/> <PanelBody content={this.props.content}/> </div> ); } }
同以前的方式相比,这个微妙的改进是革命性的。咱们新建了两个单元组件:PanelHeader 和 PanelBody。这样带来了测试的便利,咱们能够直接分离测试不一样的组件。同时,借助于 React 新的算法引擎 React Fiber,两个单元组件在渲染的效率上,乐观地预计会有较大幅度的提高。
回到问题的起点,为何一个组件会变的臃肿而复杂呢?其一是渲染元素较多且嵌套,另外就是组件内部变化较多,或者存在多种 configurations 的状况。
此时,咱们即可以将组件改造为模版:父组件相似一个模版,只专一于各类 configurations。
仍是要举例来讲,这样理解起来更加清晰。
好比咱们有一个 Comment 组件,这个组件存在多种行为或事件。同时组件所展示的信息根据用户的身份不一样而有所变化:用户是不是此 comment 的做者,此 comment 是否被正确保存,各类权限不一样等等都会引发这个组件的不一样展现行为。这时候,与其把全部的逻辑混淆在一块儿,也许更好的作法是利用 React 能够传递 React element 的特性,咱们将 React element 进行组件间传递,这样就更加像一个模版:
class CommentTemplate extends React.Component { static propTypes = { // Declare slots as type node metadata: PropTypes.node, actions: PropTypes.node, }; render() { return ( <div> <CommentHeading> <Avatar user={...}/> // Slot for metadata <span>{this.props.metadata}</span> </CommentHeading> <CommentBody/> <CommentFooter> <Timestamp time={...}/> // Slot for actions <span>{this.props.actions}</span> </CommentFooter> </div> ...
此时,咱们真正的 Comment 组件组织为:
class Comment extends React.Component { render() { const metadata = this.props.publishTime ? <PublishTime time={this.props.publishTime} /> : <span>Saving...</span>; const actions = []; if (this.props.isSignedIn) { actions.push(<LikeAction />); actions.push(<ReplyAction />); } if (this.props.isAuthor) { actions.push(<DeleteAction />); } return <CommentTemplate metadata={metadata} actions={actions} />; }
metadata 和 actions 其实就是在特定状况下须要渲染的 React element。
好比,若是 this.props.publishTime 存在,metadata 就是 <PublishTime time={this.props.publishTime} />;反正则为 <span>Saving...</span>。
若是用户已经登录,则须要渲染(即actions值为) <LikeAction /> 和 <ReplyAction />,若是是做者自己,须要渲染的内容就要加入 <DeleteAction />。
在实际开发当中,组件常常会被其余需求所污染。
好比,咱们想统计页面中全部连接的点击信息。在连接点击时,发送统计请求,同时包含此页面 document 的 id 值。常见的作法是在 Document 组件的生命周期函数 componentDidMount 和 componentWillUnmount 增长代码逻辑:
class Document extends React.Component { componentDidMount() { ReactDOM.findDOMNode(this).addEventListener('click', this.onClick); } componentWillUnmount() { ReactDOM.findDOMNode(this).removeEventListener('click', this.onClick); } onClick = (e) => { if (e.target.tagName === 'A') { // Naive check for <a> elements sendAnalytics('link clicked', { documentId: this.props.documentId // Specific information to be sent }); } }; render() { // ...
这么作的几个问题在于:
相关组件 Document 除了自身的主要逻辑:显示主页面以外,多了其余统计逻辑;
若是 Document 组件的生命周期函数中,还存在其余逻辑,那么这个组件就会变的更加含糊不合理;
统计逻辑代码没法复用;
组件重构、维护都会变的更加困难。
为了解决这个问题,咱们提出了高阶组件这个概念: higher-order components (HOCs)。不去晦涩地解释这个名词,咱们来直接看看使用高阶组件如何来重构上面的代码:
function withLinkAnalytics(mapPropsToData, WrappedComponent) { class LinkAnalyticsWrapper extends React.Component { componentDidMount() { ReactDOM.findDOMNode(this).addEventListener('click', this.onClick); } componentWillUnmount() { ReactDOM.findDOMNode(this).removeEventListener('click', this.onClick); } onClick = (e) => { if (e.target.tagName === 'A') { // Naive check for <a> elements const data = mapPropsToData ? mapPropsToData(this.props) : {}; sendAnalytics('link clicked', data); } }; render() { // Simply render the WrappedComponent with all props return <WrappedComponent {...this.props} />; } }
须要注意的是,withLinkAnalytics 函数并不会去改变 WrappedComponent 组件自己,更不会去改变 WrappedComponent 组件的行为。而是返回了一个被包裹的新组件。实际用法为:
class Document extends React.Component { render() { // ... } } export default withLinkAnalytics((props) => ({ documentId: props.documentId }), Document);
这样一来,Document 组件仍然只需关心本身该关心的部分,而 withLinkAnalytics 赋予了复用统计逻辑的能力。
高阶组件的存在,完美展现了 React 天生的复合(compositional)能力,在 React 社区当中,react-redux,styled-components,react-intl 等都广泛采用了这个方式。值得一提的是,recompose 类库又利用高阶组件,并发扬光大,作到了“脑洞大开”的事情。
React 及其周边社区的崛起,让函数式编程风靡一时,受到追捧。其中关于 decomposing 和 composing 的思想,我认为很是值得学习。同时,对开发设计的一个建议是,不要犹豫将你的组件拆分的更小、更单一,由于这样能换来强健和复用。
本文意译了David Tang的:Techniques for decomposing React components一文。
Happy Coding!
PS: 做者Github仓库,欢迎经过代码各类形式交流。