React compontent 分解技能

React 组件非常的强大和灵活,提供了强大的功能,但是随着时间的推移,它将变的越来越臃肿和繁琐。

与任何其他类型的编程相比,React 组件遵循单一功能原则( single responsibility principle),它不仅使你的组件更容易维护,更重要的是复用性。然而,如何负责一个庞大的React组件不再是一件容易的事。下面从易到难我买将有有三种方法具体讲解这个问题。

1. 分离 render()方法

这个是最常见的方法:当一个组件要呈现太多的元素,把这些元素分解为逻辑子组件是一个简单的简化方法。

一个常见而快速的方法就是在同一类中拆分render()方法并创建额外的“sub-render”方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Panel extends React.Component {
renderHeading() {
// ...
}

renderBody() {
// ...
}

render() {
return (
<div>
{this.renderHeading()}
{this.renderBody()}
</div>
);
}
}

虽然这中方法有它的用处,但这不是一个真正的拆分组件本身的方法。每个statepropsclass methods等仍然是共享的,所以很难确定哪些是由每个sub-render所使用。

真正降低复杂性的方法应该创建全新的组件。为更简单的子组件,功能组件可以用来把应用到最低:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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>
);
}
}

通过这种方式拆分还有一个微妙但重要的区别。代替直接与间接函数调用组件声明,我们以更小的单位来组织React代码。这是因为返回的‘Panel’的render()是一个树形的元素,只有走到PanelHeaderPanelBody,而不是在它们之下的所有元素。

来看个实际意义的测试:一个浅渲染可以用来轻松隔离这些单位进行独立的测试。作为奖励,当React的新语境体系到达时,规模较小的单位将使它更有效地执行增量呈现。

模版组件通过React元素作为props

当一个组件由于多个变量或者配置变得非常的复杂,考虑将这个组件转化成一个“模版”组件。这就需要一个专用的父组件集中配置它们。

例如,一个Comment组件可能有不同的actions或者 metadata显示,取决于你是不是作者,是否保存成功,或者有无权限等。而不是混合的Comment组件,在哪里以及如何呈现它的内容?用逻辑来处理所有的变化?这两个问题可以被认为是独立的。使用React通过元素的能力,不仅仅用数据作为props,来创建一个灵活的模板组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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>
);
}
}

另一个组件可以找出的独有的metadataactions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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} />;
}
}

请记住,在JSX中,组件的开始和结束标签之间的任何东西都作为特殊的子类支持传递。当正确使用时,可以特别表现出来。为了使用习惯,应该作为组件的主要内容区域保留。在评论示例中,这可能是注释本身的文本。

1
2
3
<CommentTemplate metadata={metadata} actions={actions}>
{text}
</CommentTemplate>

将常见方面提取到高阶组件中

组件常常会被与其主要并且直接相关的交叉问题所污染。

假设你只要点击Document组件中的一个链接来发送分析数据。为了使事情进一步复杂化,数据需要包括有关文档的信息,如ID。简单的解决方案可能是向DocumentcomponentDidMountcomponentWillUnmount生命周期方法添加代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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() {
// ...
}
}

这里有一些问题:

  • 该组件现在有一个额外的关注,掩盖其主要目的:显示文档。
  • 如果该组件在这些生命周期方法中具有其他逻辑,则分析代码也会变得模糊。
  • 分析代码不可重用。
  • 重新构建组件变得更加困难,因为您必须解决分析代码。

可以使用高阶分量(HOCs)来分解这样的问题。简而言之,这些函数可以应用于任何React组件,用所需的行为来包装该组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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} />;
}
}

return LinkAnalyticsWrapper;
}

重要的是要注意,该函数不会使组件变异以添加其行为,而是返回一个新的包装组件。正是这个新的包装组件用于代替原始的Document组件。

1
2
3
4
5
6
7
8
9
class Document extends React.Component {
render() {
// ...
}
}

export default withLinkAnalytics((props) => ({
documentId: props.documentId
}), Document);

请注意,可以提取要发送的数据(documentId)的具体细节作为HOCs的配置。这可以保留文档的领域知识与文档组件,以及通用的方式来监听withLinkAnalytics HOCs中的点击。

高阶组件显示了React的强大的组合性质。这里的简单示例演示了如何将似乎紧密集成的代码提取到具有单一职责的模块中。

HOCs通常用于React库,例如react-reduxstyled-componentsreact-intl。毕竟,这些库都是解决任何React应用程序的通用方面。另一个库——recompose,通过使用HOCs进一步管理从组件状态到生命周期方法的一切。

结论

React组件是高度可组合的设计。通过轻松分解和组合它们,可以利用这一点。

不要躲避创建小而重要的组件。起初它可能会让你感到尴尬,但结果将是更强大和可重用的代码。