React Lessons Learned
I’ve been playing with React for a few days now and, as always, have run across a few things that made me scratch my head and seek help from various places. In an effort to capture some of the errors and mistaken assumptions I have made, I’ll try and show them in a very simple way below. As always, comments and suggestions for improvement are more than welcome.
A Simple Page
To try and show the issues I needed a simple app that I could write that followed what I was working on closely enough to be relevant but without a lot of the complexity that obscures the basic issues. After some thought I’ve come up with a very simple visit planner. It’s going to show a simple timeline of “visits” that can be added in any order and displayed in the same order. No attempt is made to save anything, it’s just data on a page that vanishes upon refresh.
Again, to keep it simple I’m not bothering with anything beyond a straight HTML page containing everything. As I’m not travelling while writing this I’ve used CDNJS for the libraries, but have gone with the non-minified versions to allow for greater debugging. I’m not sure it gets any simpler? I’ve not bothered with any CSS.
Starting Point
Without further fanfare, this is my starting point.
Visit Planner
There’s not much to say about this, it’s simple plain React. I’m using moment for the date/time and then simply ignoring the date portion as it provides nice functions for dealing with time intervals. As it stands it doesn’t do much 🙂
Visits
For the purposes of this each visit will simply be an object that has a description (to differentiate it from the other objects), a duration and an optional additional duration (initially set to 0). I’m going to create these as seperate classes (even though they will largely be identical) as it allows me to show things clearer. In reality they wouldn’t be done this way 🙂
My first pass at getting something basic looked like this.
var Hair = React.createClass({ getInitialState: function() { return {duration: 90, additional: 0} }, render: function() { return (
Hair Appointment
Duration: {this.state.duration} minutes
Additional duration: {this.state.additional} minutes
Manicure
Duration: {this.state.duration} minutes
Additional duration: {this.state.additional} minutes
Again, nothing too fancy. The next step was to add the code to add them to the main Visit object. Obviously I would need a list of the objects, so I started by changing the initial state to
return { start: today, start_times: start_times, appointments: [] }
Next, a couple of buttons to add appointments…
The addAppointment function is also pretty basic – or so I thought…
Try #1 to add Appointments
This was my initial attempt.
addAppointment: function(ev) { var n = this.state.appointments.length; switch(ev.target.id) { case "hair-btn": { this.state.appointments.push(>); break; } case "nail-btn": { this.state.appointments.push(); break; } } this.forceUpdate(); },
Adding a line to render these out,
{ this.state.appointments }
Gives us what appears to be a working page. Click a button – appointment appears. All looks good, so lets continue to add some other functionality.
Additional Time?
Part of the idea of each appointment is to allow each to have a certain amount of time added, so lets add that by changing the plain output to an input and adding a total time output in render.
var Hair = React.createClass({ getInitialState: function() { return {duration: 90, additional: 0} }, setAdditional: function(ev) { this.setState({additional: parseInt(ev.target.value)}); }, totalTime: function() { return this.state.duration + this.state.additional; }, render: function() { return (
Hair Appointment
Duration: {this.state.duration} minutes
Additional duration: minutes
Total Time Required: {this.totalTime()} minutes
Having done that it’s now possible to add multiple objects and give each their own additional time – each is a self contained unit exactly as we’d expect. Having done that, how do we now create the timeline aspect of the main Visit object?
Timeline
The timeline is simple enough to imagine – we know the start time and how long each appointment takes, so we need to go through the and figure out a start time for each (based on either the start or the previous appointment) and then get a finish time. Changing any appointment should change all those after it, and changing the overall start time should change them all. As I want each object to be as self contained as possible, perhaps passing in the start time via the props makes sense? Changing the Hair object to allow this gave me the code below.
var Hair = React.createClass({ getInitialState: function() { return {duration: 90, additional: 0} }, setAdditional: function(ev) { this.setState({additional: parseInt(ev.target.value)}); }, totalTime: function() { return this.state.duration + this.state.additional; }, finishTime: function() { return this.props.start.clone().add(this.totalTime(), "minutes"); }, render: function() { return (
Hair Appointment
Start: {this.props.start.format("HH:mm")}
Duration: {this.state.duration} minutes
Additional duration: minutes
Total Time Required: {this.totalTime()} minutes
Finish: {this.finishTime().format("HH:mm")}
Of course, unless I supply the start time it won’t do anything…
switch(ev.target.id) { case "hair-btn": { this.state.appointments.push(); break; } case "nail-btn": { this.state.appointments.push(); break; } }
Running this looks good to start with, but there’s a problem – changing the start time doesn’t change the appointment. Changing the additional time (which forces an update) then updates the appointment and shows the correct time. Also, at present every appointment uses the same start time, so that needs fixed 🙂
Realisations
After playing with a few different things and much looking around at websites it became obvious that while react components are self contaiend objects, they can’t be used in the same way that I could use objects. So, time for a rethink.
React uses keys to determine whether an object is the same as one already rendered, so as long as the key isn’t changed objects will persist between renderings. Eah time it’s rendered I can change the props I use, so when I initially add an appointment I don’t need/want to create it, just record that enough details for it to be added during the render, probably with varying props.
Each appointment knows how long it needs to be (the fixed plus variable duration), but the main Visit object also needs to know. Time for the appointment to call the parent when something changes, so we need a callback.
First step, change the appointments to handle these changes. We need to trigger the callback in 2 instances – when the appointment is initially created (using the componentDidMount hook) and when the additional duration value is changed (via an event handler).
componentDidMount: function() { this.props.updateInterval(this.props.id, this.totalTime()); }, setAdditional: function(ev) { this.state.additional = parseInt(ev.target.value); this.props.updateInterval(this.props.id, this.totalTime()); },
I found that using this.setState(…) in setAdditional always resulted in the value being the previous one, I’m guessing because an update was triggered. This led to the callback not always being called and some very odd results initially, hence my switch to simply setting the value directly and relying on the update triggered by the parent when receiving the callback to update things.
Additional duration: minutes
With these in place, the next step is to modify the main visit class. As we’re adding in strict order and not interested in a dyanmic ordering ability, we will just use the length of the list as the ‘id’ for each appointment. We’ll also store the duration of each appointment in the data, ending up with this code.
addAppointment: function(ev) { var n = this.state.appointments.length; switch(ev.target.id) { case "hair-btn": { this.state.appointments.push({what: 'hair', key: n, interval: 0}); break; } case "nail-btn": { this.state.appointments.push({what: 'nails', key: n, interval: 0}); break; } } this.forceUpdate(); },
Now that we have the data being stored, next step is to add the callback that will update the intervals. This is a simple function that takes the index and the new interval duration, as shown below.
updateInterval: function(idx, interval) { this.state.appointments[idx].interval = interval; this.forceUpdate(); },
Finally we need to render the appointments with the correct start times.
render: function() { var _appts = []; var _start = this.state.start.clone(); this.state.appointments.map(function(app, idx) { var _props = {key: app.key, start: _start, id: app.key, updateInterval: this.updateInterval}; switch(app.what) { case "hair": { _appts.push(<Hair {..._props}/>); break; } case "nail": { _appts.push(<Nails {..._props}/>); break; } }; _start = _start.clone().add(app.intervals, "minutes"); }.bind(this)); return ( ...
We also need to render the _appts list, so one final change.
Visit starts at {this.state.start.format("HH:mm")}
{ _appts }Summary
So it all works and the way it works is surprisingly flexible, if a little different than where I started 🙂 I’m sure there are better ways of doing all this, so get in touch if you can educate me 🙂