之前分享过几篇关于React手艺栈的文章:
……
本日再来同人人议论 React 组件设想的一个风趣话题:剖析 React 组件的几种进阶要领。
React 组件魔力无限,同时灵活性超强。我们能够在组件的设想上,玩转出许多名堂。然则保证组件的Single responsibility principle: 单一准绳异常重要,它能够使得我们的组件更简朴、更轻易保护,更重要的是使得组件越发具有复用性。
然则,怎样对一个功用庞杂且痴肥的 React 组件举行剖析,或许并非一件简朴的事变。本文由浅入深,引见三个剖析 React 组件的要领。
切割 render() 要领
这是一个最轻易想到的要领:当一个组件衬着了许多元素时,就须要尝试星散这些元素的衬着逻辑。最敏捷的体式格局就是切割 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堆栈,迎接经由过程代码种种形式交换。