I am working on a ReactJS project, a calendar as an example for the concept I'm after, where the desired behavior is to display:
January 1, 2016
Activity 1
Activity 2
January 2, 2016
Activity 3
January 4, 2016
Activity 4
Activity 5
January 8, 2016
Activity 6
In other words, for the interesting part, the date of the heading, besides straightforward CSS, is intended to display before an entry once, before the first entry for a day, and then not be shown above the first entry for a day.
Facebook / React / Flux effectively declare war on shared mutable state, and an obvious but wrong approach would be to solve this through shared mutable state. What would be a better, or the best way, to approach something as above where one subcomponent should be displayed differently depending on more than its own state. This might not need shared mutable state, but I don't see what the most idiomatic approach may be.
--CLARIFICATION--
The first comment by @SeanO on this question provides (correctly, I believe) an algorithm that solves the type of problem. And I did something like this on a nice, crufty Perl CGI, years back.
But what I was really looking for is not really about what kind of algorithm would be appropriate here, but how to appropriately implement it in ReactJS.
--ADDITIONAL CLARIFICATION--
One question that I had after looking at your solution: as given, it works for static data. What I'm interested in includes having displayed UI change, not only as "tomorrow" becomes "today" and the incumbent is pulled from display, but something where people can add new data to the calendar or otherwise change it. Persisting user-initiated change is easy enough with accompanying tools, but I'm wondering here. Is the answer "Just do the same thing but move everything mutable to state," or is a different approach appropriate?
I can pull through and implement it, perhaps badly, on my present understanding but here I want to know what solution really wins. @WiktorKozlik's opening remark in his answer was exactly what I wanted to know: If a component needs to know more than the data it receives to render itself, then it's a sign that there is something wrong with that component.
Thanks,
Generally, the key to reusable components that don't depend on internal state alone is to use properties. Data flows from top-level components down into composed children. It' helps a lot to break the UI down into responsibilities (much the same way you'd utilize SRP in other situations).
For example, given your spec, it seems like there are three nested components/responsibilities:
Let's walk through building out this component. The finished example is available on JSFiddle.
Since you didn't specify the data format you're starting with, let's go with a simple array of objects with a date and an activity name each:
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" },
];
Our top-level calendar component will take data in this format as its input:
var ActivitiesCalendar = React.createClass({
propTypes: {
activities: React.PropTypes.arrayOf(
React.PropTypes.shape({
day: React.PropTypes.string.isRequired,
name: React.PropTypes.string.isRequired
})
).isRequired
},
// ...
});
The calendar should render one day entry for every day that contains one or more activities. Let's transform our array of day/activity pairs into an object that maps days into arrays of activities that occurred on that day. I use Underscore.js plus some ES6 syntax made available to us via the JSX transpiler here, but you don't have to:
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"
// ]
// }
Note that the resulting value of activitiesByDay
looks suspiciously similar to the UI we want to display. Transforming data into a format that maps to your UI structure is often a great way to approach UI composition in React.
We pass activitiesByDay
to renderActivities
, which figures out which days to render and what order to render them in by iterating over the object's keys, sorting them, and pulling out the activities for each day.
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>
);
});
}
Note that the calendar component makes no assumptions as to how a day should look; it delegates this decision to CalendarDay
, simply passing in the day and the list of activities for that day.
Similarly, the calendar day component lays out the date heading and the activities themselves, but again doesn't make a decision on what an activity actually looks like. It delegates this to the CalendarActivity
component:
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>;
}
});
I'm using Moment.js to display a nicely formatted date heading; otherwise, this pattern looks pretty similar to the ActivitiesCalendar
component.
Finally, the activity component simply displays the activity (in our case, just a string):
var CalendarActivity = React.createClass({
propTypes: {
activity: React.PropTypes.string.isRequired
},
render() {
return <div>{this.props.activity}</div>;
}
});
In the finished example, we can see the data flowing from the top-level component (the calendar) into the day and activity components as properties. At each stage, we break the data down into smaller portions and send some piece of it off to another component. Each component composes smaller, more focused components until we end at raw DOM nodes.
Also note that each of the components can stand alone—the only interface is via the component's properties, and the components are not coupled to their position in the component hierarchy. This is important for truly reusable components. For example, we could render a single calendar day by itself by looking at CalendarDay
's propTypes
and sending it some data in the shape it declares:
// 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);
There are other high-level techniques you can utilize; for example, when using the Flux pattern, it's common to have a component reach out into data stores to fetch its own data. This can simplify data fetching as you're no longer required to pass data down from parent to child to grandchild, etc., but it's worth noting that the component is less reusable in this case.
[Update] In terms of dynamic data, the canonical answer for React is to move the shared data up to some parent component and have other components that need to modify that data call functions on that component; they get access to those functions by having them passed as props. For example, you might have
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});
}
});
Children of ActivitiesCalendar
that need to be able to add an activity to the calendar should also receive a reference to the supplied onActivityAdd
prop. You would repeat for other operations, like onActivityRemove
, etc. It's important to note that the calendar components don't modify anything on this.props
directly—they only modify the data by calling some supplied function. (Note this is exactly how something like <input onChange={this.handleChange} />
works.)
Any application that does anything interesting is going to have some sort of state mutation somewhere; the point isn't to abstain from mutable state, but to minimize it, or at least control it and make modification of it understandable—rather than child components updating properties on some object, they call some provided method.
This is where the flux pattern starts to shine: flux centralizes mutable data into stores, and only allows modification of that data via events (so the components are decoupled from the implementation details of mutating the state) but doesn't force you to pass properties down long component hierarchies. That said, for reusable components, it's usually best to stick with function references as properties so the components aren't tied to a specific application or flux implementation.