React Lesson 2: Dealing with React’s Would-Be Immutable States

Let’s discuss setState further. As usual, along the way we will create a little widget and try to learn some things by doing.

Like the others, the code I make for this lesson will be hosted on my GitHub repo react-examples, which is located here: https://github.com/xerocross/react-examples. Let me include a tiny bit of git instruction here because I think people only learn git while trying to learn something else. I also created a branch for my React Lesson 1 post, but it will not be merged into master. I almost always prefer to interact with git in a command shell. To switch back to master, I execute git checkout master. While I’m here on master, I decided to remove some extraneous stuff. I committed that and pushed it. Now I create a new branch: git checkout -b setstate-horror. This both creates the branch (built on top of master) and switches me to it.

In this example, I am not going to directly transform the App component into my widget. I’m going to leave that almost unchanged and create a new component that I’ll render inside App. I’m going to call my new component MyStatefulComponent. I don’t see any built-in command line tools for scaffolding, so to create my component I’m just going to create a new directory and file directly. Fair warning: I haven’t learn naming conventions yet. I have created components/MyStatefulComponent.jsx. I refuse to call this JSX crap a JavaScript file. But I will (mostly) try to learn the conventions and attitudes of working React people before throwing them out in favor of my own style.

As discussed in React Lesson 1, the React framework forces us to explicitly build an entirely new state object every time state changes. The only reason why I could imagine wanting to do that would be to save a copy of previous states in memory—some or all—perhaps for the sake of moving backward and forward, like undo and redo commands. We’re going to lean into that and try to build something mildly interesting that uses this design pattern and which isn’t already done in detail someplace else (one hopes).

There’s a maxim that comes to mind now about how the medium of expression influences the message itself. Or you might say that what language you speak influences not just how you say something (in the literal sense) but also what you say—what your message is.

Let’s put some basic scaffolding in the new component and see if we can make it actually show up in a browser. Recall that create-react-app gives us a built-in development server (something I have sometimes called a “live” server). You start it with yarn start. In MyStatefulComponent, I throw in a render function that just returns some text: “I’m here!”.

To compose two components, I see people just throwing notation like<MyStatefulComponent /> into the template, but how and where does that get defined so the React engine can make sense of it?

Sidenote: On Googling, I found this doc: https://reactjs.org/docs/composition-vs-inheritance.html. I just read this and I love it: “At Facebook, we use React in thousands of components, and we haven’t found any use cases where we would recommend creating component inheritance hierarchies.” I definitely agree. By contrast, Angular tries way too hard to be Java, and inheritance is deeply baked into that system.

As for composing components, every example I see shows both classes in the same file. That’s not real life. They will be in separate files. Let’s import the class from the target component’s file like so: import MyStatefulComponent from "./components/MyStatefulComponent". With MyStatefulComponent in scope, now we can just directly put <MyStatefulComponent /> into our render template. This works. Now I have “I’m here!” in my browser. That means MyStatefulComponent is being rendered, so now we want to make it actually do something mildly interesting.

By the way, I’m starting to really miss TypeScript. I wonder how against-the-grain it would be to write React stuff in TypeScript. This article looks like a promising read on that topic, but we can file it away to read later: https://blog.logrocket.com/how-why-a-guide-to-using-typescript-with-react-fffb76c61614.

What if we build a cute little plain text editor with a save button and a back button so it’s easy to go back to previous save points? Sorta like git commits. You can always step back to previous commits either to just look at them or to start over from that point. But the way git works is actually very complex. Let’s keep things simple.

Obviously we need form elements like buttons and a textarea. This page is helpful: https://reactjs.org/docs/forms.html. We add this textarea to the template: <textarea onChange = {this.handleTextChange} value = {this.state.text}></textarea>. Of course we also have a this.state object that gets created in the constructor (see Lesson 1 for what I think about that) and we define a method
handleTextChange. The method fires a console log and executes this.setState({text: event.target.value}). (Don’t forget you have to explicitly bind the object to this in the constructor for the method to work right.)

So far this doesn’t do much, but if you type stuff in the textarea and look in the console, you will get a stream of updates with a new value for each new keystroke much like an Observable from over in the Angular world. As you type a sentence into that textarea, the state objects are being created and destroyed for each keystroke—and this seems to be the preferred way of doing things in React (I’m following the official docs). I’m just going to go with it for now, but it seems like we are burning processor power for no reason.

I’m not interested in every state. As I type “adam” into the textarea, I have an entirely new state object for each: “a”, “ad”, “ada” and “adam”. I want to capture a copy of the state whenever a user clicks the “commit” button. I think we could actually maintain our list of previous states IN the state object. On the surface it sounds in some way circular, but in fact there is no circularity. All the previous states are entirely distinct objects from the current state object. So the current state can hold references to them.

That said, let’s be sure not to maintain references to states that don’t interest us. Maintaining a reference to an object means the object stays in memory and cannot be garbage-collected. That would be a performance drain.

I think maybe a double-linked list is the right data structure for this widget. Here’s what I have in mind: in addition to state containing the text of our form, it will also contain a link to the previous committed state (if any) and a link to the next committed state (if any). Then we can browse through commits using back and forward buttons. Attaching the previous committed state to a new state is easy. Here’s the rub though: there is no way to know in advance what the next committed state will be when a state is created. We could attach that information later, but React seems in love with the idea of maintaining “immutable” states. That is, we are encouraged never to mutate them. Only to create new state objects.

Let’s create the “back” functionality first and then we will think on a way to browse forward again. Getting the “back” button to work wasn’t hard. Every time the user clicks “commit” we execute this.setState((state, props)=>{ return {text: state.text, previous: this.state }}). This creates a new state object and the previous state object gets attached as this.state.previous. Now if you click the back button, it simply grabs the previous state object from this.state.previous and executes this.setState (this.state.prevState) if it exists. This makes that previous state the current state again. But read on because I have reservations about that. It works, but I think it might be a violation of best practices. I’ll explain.

Should we step in the same river twice? Suppose we do it a little differently. Doing things the way I describe means that if you click back, then the new (current) state becomes the actual object that was stored in previous state. It is not a new state that looks like the old one. It’s the same object in memory. That feels like a bad practice to me. I haven’t read somewhere about it being bad practice. It just feels like a violation of the rules of this paradigm.

When the user clicks “back”, instead of literally setting our current state to be the actual previous state object, let’s make a clone. If you look at my code for this lesson, you will see that I’m doing all the clone-making by hand with object literals. I know that will get tiresome real quick. Later I’ll look into other techniques people use for that. Cloning an object is a common procedure, but there are situations where it has to be hand-tooled to the kind of object at hand.

Making a clone works. The back button still works. Plus I think now we have the answer about going forward again. It’s simple. If the user click’s back, then we grab that previous state and clone it, so we have something like clonedPreviousState. We can also grab a reference to the existingState and tack it on like so: clonedPreviousState.next = existingState. We are modifying the object
clonedPreviousState programmatically, but we’re doing that before it becomes state. With that change made, we use setState to make it our state. If we do things this way, then as the user browses backward over previous commits, he creates a double-linked list of commits.

With that in place, now at any time if you happen to be in a state where next is defined, then it is possible to go to next. So we can just make a button that causes that to happen. I’ll call the button “forward”. Having the right data structures in place made implementing a goForward method almost suspiciously easy. Going forward clones the “next” state and pushes that in the same way that going back cloned the previous state. And if there isn’t any next state, then it just does nothing.

Some weird things can happen here if you go backward to a previous commit and edit from there, try to go forward again, etc. Cleaning this up to behave as expected would require us to actually formulate how we expect it to work, which we didn’t do. We just have a toy. There weren’t any specs to build to. If this were, say, a text editor with ordinary “undo” and “redo” functions, then I think we all know if you “undo” and then dirty the input by typing a single key, then there is no “redo”. That option immediately goes away. By contrast, Git is a lot more forgiving than that—and a lot more complex. Like I said: we’re not trying to make Git. Let’s do that thing I said. Let’s make it so that if you go back and then edit something, there’s no possibility of going forward again.

To do that, in the handleTextChange method, instead of just copying the state.next value into the new state, we set it to null. That way if there is a single keystroke, then the entire linked list in the forward direction is gone. Done and done.

We could probably spend all day tinkering with this widget to make it function exactly as it should—according to whatever specs we dream up from one moment to the next—but I have a feeling we have gotten what we are going to get from this example. Lucky for us the goal was learning. Still, I might tinker a bit more. If I do anything interesting, I’ll write a Part 2 or something.

Recall that the code I wrote alongside this blog post is available on my GitHub repo react-examples in branch setstate-horror. Also, if you are so inclined, you actually can use Git to browse backward and forward through my committed changes. And Git is not a toy someone made in an hour. Git functions exactly as expected.

[Update: I didn’t like where I left our little text editor widget at the end of this post, so there is a continuation in React Lesson 2 (Part 2).]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s