1

我正在研究一个 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 在他的回答中的开场白正是我想知道的:如果一个组件需要知道的不仅仅是它接收到的数据来呈现自己,那么这表明该组件有问题。

谢谢,

4

2 回答 2

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>
于 2015-03-07T04:31:24.420 回答
2

通常,不依赖于内部状态的可重用组件的关键是使用属性。数据从顶层组件向下流入组合子组件。将 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 组合的好方法。

我们传递activitiesByDayrenderActivities,它通过迭代对象的键、对它们进行排序并提取每天的活动来确定要渲染的日期以及渲染它们的顺序。

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} />工作方式。)

任何做任何有趣事情的应用程序都会在某处发生某种状态突变;关键不是要避免可变状态,而是要最小化它,或者至少控制它并使修改它可以理解——而不是子组件更新某个对象的属性,它们调用一些提供的方法。

这就是通量模式开始大放异彩的地方:通量将可变数据集中到存储中,并且只允许通过事件修改该数据(因此组件与改变状态的实现细节解耦)但不强制您传递属性向下长组件层次结构。也就是说,对于可重用的组件,通常最好坚持使用函数引用作为属性,这样组件就不会与特定的应用程序或通量实现绑定。

于 2015-03-07T23:52:01.330 回答