通常,不依赖于内部状态的可重用组件的关键是使用属性。数据从顶层组件向下流入组合子组件。将 UI 分解为职责有很大帮助(与在其他情况下使用 SRP 的方式非常相似)。
例如,根据您的规范,似乎存在三个嵌套组件/职责:
- Calendar组件,负责决定要渲染的日期以及如何布置它们。
- Day组件,负责呈现日期标题和包含的活动。
- Activity组件,负责显示单个活动。
让我们逐步构建这个组件。完成的示例可在 JSFiddle 上找到。
由于您没有指定开始使用的数据格式,因此让我们使用一个简单的对象数组,每个对象都有一个日期和一个活动名称:
var activities = [
{ day: "2016-01-01", name: "Activity 1" },
{ day: "2016-01-01", name: "Activity 2" },
{ day: "2016-01-02", name: "Activity 3" },
{ day: "2016-01-04", name: "Activity 4" },
{ day: "2016-01-04", name: "Activity 5" },
{ day: "2016-01-08", name: "Activity 6" },
];
日历组件
我们的顶级日历组件将采用这种格式的数据作为其输入:
var ActivitiesCalendar = React.createClass({
propTypes: {
activities: React.PropTypes.arrayOf(
React.PropTypes.shape({
day: React.PropTypes.string.isRequired,
name: React.PropTypes.string.isRequired
})
).isRequired
},
// ...
});
日历应为包含一项或多项活动的每一天呈现一天条目。让我们将我们的天/活动对数组转换为一个对象,该对象将天映射到当天发生的活动数组。我使用Underscore.js加上一些通过 JSX 转译器提供给我们的 ES6 语法,但您不必:
render() {
var activitiesByDay = _.chain(this.props.activities)
.groupBy((activity) => activity.day)
.mapObject((activities) => _.pluck(activities, "name"))
.value();
return <ul>{this.renderActivities(activitiesByDay)}</ul>;
},
// activitiesByDay looks like:
// {
// "2016-01-01": [
// "Activity 1",
// "Activity 2"
// ],
// "2016-01-02": [
// "Activity 3"
// ],
// "2016-01-04": [
// "Activity 4",
// "Activity 5"
// ],
// "2016-01-08": [
// "Activity 6"
// ]
// }
请注意,结果值activitiesByDay
看起来与我们要显示的 UI 非常相似。将数据转换为映射到 UI 结构的格式通常是在 React 中处理 UI 组合的好方法。
我们传递activitiesByDay
给renderActivities
,它通过迭代对象的键、对它们进行排序并提取每天的活动来确定要渲染的日期以及渲染它们的顺序。
renderActivities(activitiesByDay) {
// compareDays sorts dates in ascending order
var days = Object.keys(activitiesByDay).sort(compareDays);
return days.map((day) => {
return (
<li key={day}>
<CalendarDay day={day} activities={activitiesByDay[day]} />
</li>
);
});
}
请注意,日历组件不对一天的外观做出任何假设;它将此决定委托给CalendarDay
,只需传递当天和当天的活动列表即可。
日组件
同样,日历日组件列出了日期标题和活动本身,但同样不会决定活动的实际外观。它将这个委托给CalendarActivity
组件:
var CalendarDay = React.createClass({
propTypes: {
day: React.PropTypes.string.isRequired,
activities: React.PropTypes.arrayOf(React.PropTypes.string).isRequired
},
render() {
return (
<div>
<strong>{this.formattedDate(this.props.day)}</strong>
<ul>
{this.props.activities.map(this.renderActivity)}
</ul>
</div>
);
},
formattedDate(date) {
return moment(date).format("MMMM DD, YYYY");
},
renderActivity(activity) {
return <li key={activity}><CalendarActivity activity={activity} /></li>;
}
});
我正在使用Moment.js显示格式良好的日期标题;否则,此模式看起来与组件非常相似ActivitiesCalendar
。
活动组件
最后,活动组件简单地显示活动(在我们的例子中,只是一个字符串):
var CalendarActivity = React.createClass({
propTypes: {
activity: React.PropTypes.string.isRequired
},
render() {
return <div>{this.props.activity}</div>;
}
});
结论
在完成的示例中,我们可以看到数据从顶级组件(日历)流入日期和活动组件作为属性。在每个阶段,我们将数据分解成更小的部分,并将其中的一部分发送到另一个组件。每个组件都由更小、更集中的组件组成,直到我们以原始 DOM 节点结束。
另请注意,每个组件都可以独立 - 唯一的接口是通过组件的属性,并且组件不耦合到它们在组件层次结构中的位置。这对于真正可重用的组件很重要。例如,我们可以通过查看CalendarDay
'spropTypes
并以它声明的形状向它发送一些数据来单独渲染一个日历日:
// CalendarDay's propTypes is:
//
// day: React.PropTypes.string.isRequired,
// activities: React.PropTypes.arrayOf(React.PropTypes.string).isRequired
var day = <CalendarDay day="2015-12-09"
activities={["Open gifts", "Blow out candle", "Eat cake"]} />
React.render(day, document.body);
JSFiddle 示例
您还可以使用其他高级技术;例如,在使用 Flux 模式时,通常让组件进入数据存储区以获取它自己的数据。这可以简化数据获取,因为您不再需要将数据从父级传递到子级到孙子级等,但值得注意的是,在这种情况下组件的可重用性较低。
[更新] 在动态数据方面,React 的规范答案是将共享数据移动到某个父组件,并让其他组件需要修改该组件上的数据调用函数;他们通过将它们作为道具传递来访问这些功能。例如,您可能有
var Application = React.createClass({
getInitialState() {
return { activities: myActivities };
},
render() {
return <ActivitiesCalendar activities={this.state.activities}
onActivityAdd={this.handleActivityAdd}
... />;
},
handleActivityAdd(newActivity) {
var newData = ...;
this.setState({activities: newData});
}
});
ActivitiesCalendar
需要能够将活动添加到日历的孩子也应该收到对提供的onActivityAdd
道具的引用。您将重复其他操作,例如onActivityRemove
等。重要的是要注意日历组件不会this.props
直接修改任何内容- 它们仅通过调用某些提供的函数来修改数据。(请注意,这正是类似的<input onChange={this.handleChange} />
工作方式。)
任何做任何有趣事情的应用程序都会在某处发生某种状态突变;关键不是要避免可变状态,而是要最小化它,或者至少控制它并使修改它可以理解——而不是子组件更新某个对象的属性,它们调用一些提供的方法。
这就是通量模式开始大放异彩的地方:通量将可变数据集中到存储中,并且只允许通过事件修改该数据(因此组件与改变状态的实现细节解耦)但不强制您传递属性向下长组件层次结构。也就是说,对于可重用的组件,通常最好坚持使用函数引用作为属性,这样组件就不会与特定的应用程序或通量实现绑定。