ReactJS和父/子组件:如何构建这个概念日历?

时间:2015-03-06 17:37:09

标签: javascript reactjs hierarchy uicomponents

我正在研究一个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

换句话说,对于有趣的部分,除了简单的CSS之外,标题的日期旨在在一个条目之前,在第一个条目之前显示一天,然后不显示在第一个条目的上方。一天。

Facebook / React / Flux在共享可变状态上有效地宣战,一种明显但错误的方法是通过共享可变状态来解决这个问题。什么是更好或最好的方法来接近上面的东西,其中一个子组件应该根据其自身状态而不同地显示。这可能不需要共享的可变状态,但我不知道最惯用的方法是什么。

- 澄清 -

@SeanO对这个问题的第一个评论提供了(正确的,我相信)一种解决问题类型的算法。几年前,我在漂亮,狡猾的Perl CGI上做了类似的事情。

但我真正想要的并不是关于什么样的算法适合这里,而是如何在ReactJS中适当地实现它。

- 其他澄清 -

在查看解决方案后我遇到的一个问题:如果给出,它适用于静态数据。我感兴趣的内容包括显示UI更改,不仅仅是"明天"成为"今天"并且现任者被从显示器中拉出,但人们可以将新数据添加到日历或以其他方式更改它。使用随附的工具可以很容易地保持用户启动的更改,但我想知道这里。是答案"只是做同样的事情,但把一切都变成了状态,"或者是一种适当的不同方法?

我可以接受并实施它,或许很糟糕,根据我目前的理解,但在这里我想知道什么解决方案真的赢了。 @ WiktorKozlik在他的回答中的开场白正是我想知道的:如果一个组件需要知道的不仅仅是它收到的数据来呈现自己,那么它就表明存在错误使用该组件。

谢谢,

2 个答案:

答案 0 :(得分:3)

如果一个组件需要知道的不仅仅是它收到的数据来渲染自己,那么它就表明该组件有问题。我想知道您是否尝试将组件与您拥有的数据(活动)的形状相匹配,而不是从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>

答案 1 :(得分:2)

通常,不依赖于内部状态的可重用组件的关键是使用属性。数据从顶级组件流向组合子组。它&#39;帮助很多将UI分解为责任(就像你在其他情况下利用SRP一样)。

例如,根据您的规范,似乎有三个嵌套组件/职责:

  • 日历组件,负责决定渲染的天数以及如何布局。
  • 组件,负责呈现日期标题和所包含的活动。
  • 活动组件,负责显示单个活动。

让我们逐步构建这个组件。完成的示例是available on 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>;
  }
});

结论

the finished example中,我们可以看到数据从顶级组件(日历)流向日期和活动组件作为属性。在每个阶段,我们将数据分解为更小的部分,并将其中的一部分发送到另一个组件。每个组件组成更小,更集中的组件,直到我们在原始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);

JSFiddle example

您可以使用其他高级技术;例如,当使用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} />之类的工作方式。)

任何有趣的应用程序都会出现某种状态变异某处;重点是不要放弃可变状态,但要最小化它,或者至少控制它并使其可修改是可理解的 - 而不是子组件更新某些对象的属性,它们会调用一些提供的方法。

这是磁通模式开始闪耀的地方:磁通将可变数据集中到存储中,并且只允许通过事件修改该数据(因此组件与变异状态的实现细节分离)但不是强制您将属性传递给长组件层次结构。也就是说,对于可重用的组件,通常最好将功能引用作为属性,因此组件不依赖于特定的应用程序或通量实现。