I decided I didn’t like how we left the little widget we created in React Lesson 2. Recall it was a little plain text editor where you can commit changes whenever you want (kinda like saving) and then you can browse back to view previous commits and then browse forward again. But we could have made it function better, and that is what we will do now. The source code is on GitHub here: https://github.com/xerocross/react-examples. It is on the branch called setstate-horror.
Where I previously left this widget, odd things happened if you went backward while the textarea input was “dirty” (to borrow terminology from Angular). Simply put, we need to keep track of whether the text as been altered since the most recent commit. That is the property I’m calling “dirty”. The simplest way to keep track of that as I see it is to just add a dirty property to our state object. When state is first created in the controller, state.dirty = false. Any onChange coming from the textarea will update the state so that state.dirty = true. A commit will update the state so that state.dirty = false.
This allows us to narrow possible user paths into just the ones that actually make sense. For example, now we have a good way to avoid accidental duplicate commits by pressing the button twice in a row. If state.dirty = false, then there is nothing new to commit, so we can just deactivate the commit button. It’s also good practice to put checks like that in the methods too even though in principle with the button disabled the methods should not be called.
Previously, we also had a problem where if the form was dirty and you click the back button, it would go back and also implicitly commit the dirty contents of the form. Say you commit the text “apple” and then you type more text so now it reads “apple, pear”. You don’t commit, but you click the back button. As of where we left things at the end of Lesson 2, the value “apple, pear” gets implicitly committed, and if you click forward now it takes us back to that value. In my opinion, that is surprising. And surprises are bad. So let’s narrow the possible user paths. What do we want to happen if the form is dirty and a user clicks “back”?
My solution to this problem is to force the user to either explicitly commit current changes before going back or else explicitly reset to the most recent commit. This means we have to add a “reset to last commit” button and a method to implement this functionality. This is a hard reset. Current changes in the dirty form will be irretrievably lost.
This territory is not getting complex exactly, but making this thing work all together will be at least twice as complex as what we had before. This new way of thinking about commits and dirty forms means we must make some small changes to every one of our calls to setState. For example, now when you click commit, the handler does not change the state.previous value. Instead, we watch for changes in the textarea, and using the state.dirty property, we can funnel these changes through different handlers depending on whether this is the first change after a commit or not. If it is the first change after a commit, then we set state.dirty = true and now what was previously state becomes state.previous.
Another way of saying that: if you click commit and then leave the form as it is, the only thing that has changed is the value of state.dirty. We have not actually stored this committed state anywhere because it is still for all practical purposes the state displayed on the screen. Technically though it is a different object in memory.
Also, with this structure in place it is quite easy to implement a reset method to take us back to the last commit. It should only do anything at all if the state is dirty, so we check that. If state.dirty == true, then we know that state.previous points to the most recent commit. So we just clone those values back into the state and set dirty to false again. This puts us in the same place as we would be if we had done nothing at all after committing.
With all that done, this little widget seems to function as expected. It’s even pretty intuitive. For a nicer experience, we would probably want to bind the commit button to a keystroke. Maybe later. Also, this little widget is complex enough that some unit testing is in order. That might be the topic of Part 3 later on.
[Update: continued in Part 3.]