ReactJS 和 parent / child 组件:如何构建这个概念日历?
ReactJS and parent / child components: How do I build out this conceptual calendar?
我正在做一个 ReactJS 项目,一个日历作为我所追求的概念的示例,其中要显示所需的行为:
January 1, 2016
Activity 1
Activity 2
January 2, 2016
Activity 3
January 4, 2016
Activity 4
Activity 5
January 8, 2016
Activity 6
换句话说,有趣的是,标题的日期除了straightforward CSS之外,还打算在一个词条之前显示一次,在第一个词条之前显示一天, 然后一天不显示在第一个条目上方。
Facebook/React/Flux 在共享可变状态上有效地声明 war,一个明显但错误的方法是通过共享可变状态来解决这个问题。什么是更好或最好的方法来处理上面的内容,其中一个子组件应该根据其自身状态以外的更多内容进行不同显示。这可能不需要共享可变状态,但我看不出最惯用的方法是什么。
--澄清--
@SeanO 对这个问题的第一条评论提供了(我相信是正确的)解决问题类型的算法。几年前,我在一个漂亮、笨拙的 Perl CGI 上做了类似的事情。
但我真正要寻找的并不是什么类型的算法在这里是合适的,而是如何在 ReactJS 中适当地实现它。
--补充说明--
我在查看您的解决方案后提出的一个问题:如前所述,它适用于静态数据。我感兴趣的包括显示 UI 的变化,不仅是 "tomorrow" 变成 "today" 并且现任者从显示中撤出,而且人们可以在其中向日历添加新数据或否则改变它。使用随附的工具可以很容易地保持 user-initiated 更改,但我在这里想知道。答案是 "Just do the same thing but move everything mutable to state," 还是其他方法合适?
根据我目前的理解,我可以完成并实施它,也许很糟糕,但在这里我想知道哪种解决方案真正获胜。 @WiktorKozlik 在他的回答中的开场白正是我想知道的:如果一个组件需要知道的不仅仅是它接收到的数据来呈现自己,那么这表明该组件有问题。
谢谢,
如果一个组件需要知道比它接收到的数据更多的信息来呈现自己,那么这表明该组件有问题。我想知道您是否尝试将组件与您拥有的数据(活动)的形状相匹配,而不是从 UI 设计中派生组件结构。
例如,用下面的结构表示 UI 可以让您考虑每个组件需要什么数据,而不必担心算法。
<Calendar activities={...}>
<CalendarDay day="2016-01-01" activities={[activity1, activity2]}>
January 1, 2016
<Activity activity={activity1}>
Activity 1
</Activity>
<Activity activity={activity2}>
Activity 2
</Activity>
</CalendarDay>
<CalendarDay day="2016-01-02" activities={[activity3]}>
January 2, 2016
<Activity activity={activity3}>
Activity 3
</Activity>
</CalendarDay>
...
</Calendar>
通常,不依赖于内部状态的可重用组件的关键是使用属性。数据从 top-level 组件向下流入组合 children。将 UI 分解为职责(与您在其他情况下使用 SRP 的方式大致相同)对 有很大帮助。
例如,根据您的规范,似乎有三个嵌套 components/responsibilities:
- Calendar 组件,负责决定渲染哪几天以及如何布置它们。
- Day 组件,负责呈现日期标题和包含的活动。
- Activity组件,负责显示单个activity.
让我们逐步构建这个组件。完成的例子是available on JSFiddle.
由于您没有指定开始时使用的数据格式,让我们使用一个简单的 object 数组,每个数组都有一个日期和一个 activity 名称:
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" },
];
日历组件
我们的 top-level 日历组件将采用这种格式的数据作为其输入:
var ActivitiesCalendar = React.createClass({
propTypes: {
activities: React.PropTypes.arrayOf(
React.PropTypes.shape({
day: React.PropTypes.string.isRequired,
name: React.PropTypes.string.isRequired
})
).isRequired
},
// ...
});
日历应该为包含一项或多项活动的每一天呈现一个条目。让我们将 day/activity 对数组转换为 object,将日期映射到当天发生的活动数组。我使用 Underscore.js 加上一些 ES6 语法,这些语法通过此处的 JSX 转译器提供给我们,但您不必:
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
,后者通过遍历 object 的键、对它们进行排序以及确定要渲染的日期以及渲染它们的顺序拉出每天的活动。
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
,只需传入当天和当天的活动列表即可。
日组件
同样,日历日组件会列出日期标题和活动本身,但同样不会决定 activity 的实际外观。它将此委托给 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
组件非常相似。
Activity 组件
最后,activity 组件只显示 activity(在我们的例子中,只是一个字符串):
var CalendarActivity = React.createClass({
propTypes: {
activity: React.PropTypes.string.isRequired
},
render() {
return <div>{this.props.activity}</div>;
}
});
结论
在 the finished example 中,我们可以看到数据从 top-level 组件(日历)流入日和 activity 组件作为属性。在每个阶段,我们将数据分解成更小的部分,并将其中的一部分发送到另一个组件。每个组件组成更小、更集中的组件,直到我们以原始 DOM 个节点结束。
另请注意,每个组件都可以独立 - 唯一的接口是通过组件的属性,并且组件与它们在组件层次结构中的位置无关。这对于真正可重用的组件很重要。例如,我们可以通过查看 CalendarDay
的 propTypes
并以其声明的形状向其发送一些数据来单独呈现单个日历日:
// 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);
您还可以使用其他 high-level 技巧;例如,当使用 Flux 模式时,组件通常会访问数据存储以获取其自己的数据。这可以简化数据获取,因为您不再需要将数据从 parent 传递到 child 再到 grandchild 等,但值得注意的是该组件的可重用性较低这种情况。
[更新] 在动态数据方面,React 的规范答案是将共享数据移动到某个 parent 组件,并让其他需要修改该数据的组件调用该组件上的函数;他们通过将它们作为道具传递来访问这些功能。例如,您可能
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});
}
});
需要能够将 activity 添加到日历的 Children of ActivitiesCalendar
也应该收到对提供的 onActivityAdd
道具的引用。您将重复其他操作,例如 onActivityRemove
等。重要的是要注意日历组件 不会直接修改 this.props
上的任何内容 — 它们只会修改通过调用一些提供的函数来获取数据。 (请注意,这正是 <input onChange={this.handleChange} />
之类的工作原理。)
任何做有趣事情的应用程序都会有某种状态突变某处;关键不是要避免可变状态,而是要最小化它,或者至少控制它并使它的修改易于理解——而不是 child 组件更新某些 object 上的属性,他们调用 s我提供的方法。
这就是 flux 模式开始发光的地方:flux 将可变数据集中到存储中,并且只允许通过事件修改该数据(因此组件与改变状态的实现细节分离)但不允许迫使您将属性向下传递到长组件层次结构中。也就是说,对于可重用组件,通常最好坚持将函数引用作为属性,这样组件就不会绑定到特定的应用程序或通量实现。
我正在做一个 ReactJS 项目,一个日历作为我所追求的概念的示例,其中要显示所需的行为:
January 1, 2016
Activity 1
Activity 2
January 2, 2016
Activity 3
January 4, 2016
Activity 4
Activity 5
January 8, 2016
Activity 6
换句话说,有趣的是,标题的日期除了straightforward CSS之外,还打算在一个词条之前显示一次,在第一个词条之前显示一天, 然后一天不显示在第一个条目上方。
Facebook/React/Flux 在共享可变状态上有效地声明 war,一个明显但错误的方法是通过共享可变状态来解决这个问题。什么是更好或最好的方法来处理上面的内容,其中一个子组件应该根据其自身状态以外的更多内容进行不同显示。这可能不需要共享可变状态,但我看不出最惯用的方法是什么。
--澄清--
@SeanO 对这个问题的第一条评论提供了(我相信是正确的)解决问题类型的算法。几年前,我在一个漂亮、笨拙的 Perl CGI 上做了类似的事情。
但我真正要寻找的并不是什么类型的算法在这里是合适的,而是如何在 ReactJS 中适当地实现它。
--补充说明--
我在查看您的解决方案后提出的一个问题:如前所述,它适用于静态数据。我感兴趣的包括显示 UI 的变化,不仅是 "tomorrow" 变成 "today" 并且现任者从显示中撤出,而且人们可以在其中向日历添加新数据或否则改变它。使用随附的工具可以很容易地保持 user-initiated 更改,但我在这里想知道。答案是 "Just do the same thing but move everything mutable to state," 还是其他方法合适?
根据我目前的理解,我可以完成并实施它,也许很糟糕,但在这里我想知道哪种解决方案真正获胜。 @WiktorKozlik 在他的回答中的开场白正是我想知道的:如果一个组件需要知道的不仅仅是它接收到的数据来呈现自己,那么这表明该组件有问题。
谢谢,
如果一个组件需要知道比它接收到的数据更多的信息来呈现自己,那么这表明该组件有问题。我想知道您是否尝试将组件与您拥有的数据(活动)的形状相匹配,而不是从 UI 设计中派生组件结构。
例如,用下面的结构表示 UI 可以让您考虑每个组件需要什么数据,而不必担心算法。
<Calendar activities={...}>
<CalendarDay day="2016-01-01" activities={[activity1, activity2]}>
January 1, 2016
<Activity activity={activity1}>
Activity 1
</Activity>
<Activity activity={activity2}>
Activity 2
</Activity>
</CalendarDay>
<CalendarDay day="2016-01-02" activities={[activity3]}>
January 2, 2016
<Activity activity={activity3}>
Activity 3
</Activity>
</CalendarDay>
...
</Calendar>
通常,不依赖于内部状态的可重用组件的关键是使用属性。数据从 top-level 组件向下流入组合 children。将 UI 分解为职责(与您在其他情况下使用 SRP 的方式大致相同)对 有很大帮助。
例如,根据您的规范,似乎有三个嵌套 components/responsibilities:
- Calendar 组件,负责决定渲染哪几天以及如何布置它们。
- Day 组件,负责呈现日期标题和包含的活动。
- Activity组件,负责显示单个activity.
让我们逐步构建这个组件。完成的例子是available on JSFiddle.
由于您没有指定开始时使用的数据格式,让我们使用一个简单的 object 数组,每个数组都有一个日期和一个 activity 名称:
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" },
];
日历组件
我们的 top-level 日历组件将采用这种格式的数据作为其输入:
var ActivitiesCalendar = React.createClass({
propTypes: {
activities: React.PropTypes.arrayOf(
React.PropTypes.shape({
day: React.PropTypes.string.isRequired,
name: React.PropTypes.string.isRequired
})
).isRequired
},
// ...
});
日历应该为包含一项或多项活动的每一天呈现一个条目。让我们将 day/activity 对数组转换为 object,将日期映射到当天发生的活动数组。我使用 Underscore.js 加上一些 ES6 语法,这些语法通过此处的 JSX 转译器提供给我们,但您不必:
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
,后者通过遍历 object 的键、对它们进行排序以及确定要渲染的日期以及渲染它们的顺序拉出每天的活动。
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
,只需传入当天和当天的活动列表即可。
日组件
同样,日历日组件会列出日期标题和活动本身,但同样不会决定 activity 的实际外观。它将此委托给 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
组件非常相似。
Activity 组件
最后,activity 组件只显示 activity(在我们的例子中,只是一个字符串):
var CalendarActivity = React.createClass({
propTypes: {
activity: React.PropTypes.string.isRequired
},
render() {
return <div>{this.props.activity}</div>;
}
});
结论
在 the finished example 中,我们可以看到数据从 top-level 组件(日历)流入日和 activity 组件作为属性。在每个阶段,我们将数据分解成更小的部分,并将其中的一部分发送到另一个组件。每个组件组成更小、更集中的组件,直到我们以原始 DOM 个节点结束。
另请注意,每个组件都可以独立 - 唯一的接口是通过组件的属性,并且组件与它们在组件层次结构中的位置无关。这对于真正可重用的组件很重要。例如,我们可以通过查看 CalendarDay
的 propTypes
并以其声明的形状向其发送一些数据来单独呈现单个日历日:
// 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);
您还可以使用其他 high-level 技巧;例如,当使用 Flux 模式时,组件通常会访问数据存储以获取其自己的数据。这可以简化数据获取,因为您不再需要将数据从 parent 传递到 child 再到 grandchild 等,但值得注意的是该组件的可重用性较低这种情况。
[更新] 在动态数据方面,React 的规范答案是将共享数据移动到某个 parent 组件,并让其他需要修改该数据的组件调用该组件上的函数;他们通过将它们作为道具传递来访问这些功能。例如,您可能
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});
}
});
需要能够将 activity 添加到日历的 Children of ActivitiesCalendar
也应该收到对提供的 onActivityAdd
道具的引用。您将重复其他操作,例如 onActivityRemove
等。重要的是要注意日历组件 不会直接修改 this.props
上的任何内容 — 它们只会修改通过调用一些提供的函数来获取数据。 (请注意,这正是 <input onChange={this.handleChange} />
之类的工作原理。)
任何做有趣事情的应用程序都会有某种状态突变某处;关键不是要避免可变状态,而是要最小化它,或者至少控制它并使它的修改易于理解——而不是 child 组件更新某些 object 上的属性,他们调用 s我提供的方法。
这就是 flux 模式开始发光的地方:flux 将可变数据集中到存储中,并且只允许通过事件修改该数据(因此组件与改变状态的实现细节分离)但不允许迫使您将属性向下传递到长组件层次结构中。也就是说,对于可重用组件,通常最好坚持将函数引用作为属性,这样组件就不会绑定到特定的应用程序或通量实现。