Michael Brown April 18 2015 12:05:33 AM
As threatened in my previous post,
Basic ReactJS Tutorial, I've now updated my sample ReactJS checkboxes app into "a kind of questions and answers, survey form thing".
This post will walk through the steps that I went through to achieve that. As you read this explanation, I hope it comes across to you just how easy this was for me to do. Not because I'm any genius coder, or even that much of an expert in ReactJS yet. It's just the way that ReactJS leads you into developing your app, especially the compositional, component based design that it encourages. While it would be a stretch to say that the code wrote itself, it did pretty much design
itself. That's to say that before I sat down to type a single line of code, I already knew pretty much how I it was all going to break down. In ReactThink (and I may just trademark that term) it means that knowing the components that created in my from the previous exercise, I just needed some higher level ones to put those existing components to work a bit more.
The highest level React component that I was left with from my previous exercise was CheckBoxInputField. This component groups together a number of CheckBoxInput components, to give us a "field" of HTML input elements of type "checkbox". The number of elements in the field is determined by the array of data (or props) that we passed into the CheckboxInputField component.
The next step to turning this single question in to list of questions - i.e. a survey - is to create a new React component called CheckboxInputFields. The clue to what it does is in the name, or is intended to be so anyway! CheckboxInputFields is a plural of CheckboxInputField, so guess what it does? Yep, the CheckboxInputFields component renders a number of CheckboxInputField components. How many CheckboxInputField components the CheckboxInputFields component renders is determined, once again, by the array data that is passed into it as props.
Enough blurb. Let's look at some code.
Example 1 - React Survey App, state in wrong place
You'll see that my source data is now an array of questions and answers, stored in the global variable, questions
. This data is passed into the CheckboxInputFields component, as props, by another React component called SurveyApp (of which more later). The CheckboxInputFields component's render method maps through the questions array and creates a new CheckboxInputField component for each member, passing that member down as props to each new CheckboxInputField. Each CheckboxInputField component so created is pushed onto an array variable called mappedInputFields. Finally, the mappedInputFields variable, which is now a collection of CheckboxInputField components, is rendered via the return statement.
I've no idea how easy that all looks to you, but I can tell you it took me about two minutes to type it out, and it ran perfectly the first time. Furthermore, it did so without me having me to change anything in the CheckboxInputField component. In fact, I'm not sure that I even had to look
at the CheckboxInputField component!!! I knew what that CheckboxInputField component does and what its props were from last time, so no real need to look at it again now. Cool as!
(Actually, I did modify CheckboxInputField later on, to have it render the question - or the blurb
property as I've called it - just before the list of answers.)
That pesky state again
In my earlier post, I said the following:
This is one of React's underlying principles: state should be handled at the highest level possible in your application. High level state is then passed down to lower level components as props.
Is that how state - i.e. are the checkboxes checked or unchecked? - is being handled in this new version of the app? Most definitely not! The state is still being handled down at the CheckboxInputField level. The CheckboxInputFields component has no easy way of knowing whether any of the CheckboxInputField components that it created have been checked by a user. Once again, the state is being handled at too low a level. State needs to be handled at the CheckboxInputFields level.
Or does it?
Thinking ahead, what if we want our app to display a collection of CheckboxInputFields components? In other words, we want a survey to consist of a collection of different groups of questions, each grouped by a theme or topic? In that case, our state would again be at too low a level if it were handled in the CheckboxInputFields component. We've have to refactor yet again to move the state to our new, higher level. So let's skip that mistake and do it right first time, this time.
If you want something done, then go to the top!
I only have one CheckboxInputFields element in my Example 1 version above, but I never intended to handle state within it. Instead, I've created a higher level React component, which is the SurveyApp component. In Example 1, all it does is render my one CheckboxInputFields element with the necessary props, and nothing more. But it was my intention from the beginning to handle my state in this top most element, so let's do that now.
Example 2 - React Survey App, state in correct place
In Example 2, state is handled in the SurveyApp component. The state data is updated and setState() call is made within that component. A setState call causes the SurveyApp to re-render, which causes the whole app to re-render, like so:
SurveyApp -> CheckboxInputFields -> CheckboxInputField -> CheckboxInput
With each CheckboxInputFields owning multiple CheckboxInputField components, and each CheckboxInputField owning multiple CheckboxInput components, the diagram of re-renders would actually look like a pyramid. (I just couldn't be arsed to draw one!)
And if you're thinking "re-rendering the whole app = performance nightmare", remember that React's virtual DOM and super fast diffing engine mean that this isn't the case.
Passing the buck upwards again
In order to have the SurveyApp component handle the state, I did have to rewrite the lower level components to ensure that the when events are triggered down there (i.e the user checks or unchecks a checkbox) the relevant information from that event reaches up to SurveyApp level.
CheckboxInput required no changes because it was already passing its state changes upwards (to CheckboxInputField).
CheckboxInputField gets the biggest rewrite because this is where we were handling state previously. So, its getInitialState() method is removed. Instead of looping through the (now non-existent) state array, its render() method now maps through an array of passed in props. CheckboxInputField's handleFieldChange() method, which was resetting the state earlier, now simply passes the relevant data, with a little preprocessing, back up to CheckboxInputFields via the supplied (via props) callback function, i.e. this.props.handleChange().
CheckboxInputFields does even less with that data! There's no equivalent handleChange() or handleFieldChange() method in CheckboxInputFields. The callback function that was passed down from CheckboxInputFields to CheckboxInputField is actually one that was originally passed from from SurveyApp to CheckboxInputFields. So when the new event data is heading back upwards, the CheckboxInputFields component simply hands it on up to SurveyApp. Think if of it as like how a passthrough server works.
Finally, the new event data gets all the way up to SurveyApp's handleFieldChange() method, where the state is reset. The method uses two passed up indexes, the index of the question where an element has been (un)checked and the index of element (i.e. which answer) within
that question, to determine which data point to change. The change is to add a .checked property if the underlying checkbox field has been ticked, and to delete that property if the field has been unticked. Once the relevant data point is modified, we call a setState(), so re-rendering our whole app, and our work here is done.