States Within States (Part 1)

This is a direct continuation from my React Widget from 0 to Launch, which in turn grew out of my React Lesson 2.

Since those posts, I have a cute little widget hosted at https://xero-draft.herokuapp.com/ by Heroku. The source is hosted here:
https://github.com/xerocross/xero.draft. I have been thinking of it as a tool for drafting short text documents like emails with an undo/redo system that is a little more robust than typical text editor fare.

This widget is super simple. Simple is not a bad thing, but somewhere in between this widget as you see it and recreating Git from scratch, I think there are a few more features that would really make this widget useful. Here’s some cool stuff we could do. Some of it would be easy. Some might prove quite difficult.

  • Add a keyboard shortcut for the “commit” button, and maybe others. But I think probably just commit. I don’t think binding the left and right arrows to “back” and “forward” would serve anyone well.
  • Make it work on mobile devices. I just need to slightly alter some styles for that—at least to start. Really I’m correcting a mistake here. I do believe in mobile first. I just didn’t realize I had violated that. It’s why companies have peer review.
  • Make it so when you browse from one commit to the next the actual changes you made are highlighted. Git can do something like this for you, and I really have no idea how Git does it. I wouldn’t try to reproduce anything so complex as whatever Git does, but even a relatively simple implementation of this idea doesn’t sound easy. It would be a nice feature though.
  • Use local storage to maintain state data so if the user browses away and comes back, his data isn’t lost. (In a more complex kind of app, this sort of data would be stored on the server, but we definitely won’t go that far any time soon.)

An important note in passing: typically when making a practice widget like this, I don’t use any libraries for handling the work for me unless learning those libraries is part of the challenge—or if I wrote the library. So it can go either way, but if I decide to build something from scratch rather than use a perfectly fine library—that’s why. This is not good practice for building a production, business critical app though. In that case, use something battle-tested.

Let’s talk about the task above, the persisting to local storage thing. It’s an almost magical little perk of HTML5, saving one’s work automatically in the background, locally. When done correctly, the user can leave and come back, refresh, etc—and all his work is just as when he left it. It’s something I’ve done in several of my widgets on WidgetWonk. Those widgets just automatically remember your data, but the data is on your computer, not the server.

Also, this topic will allow us to explore the notion of state in more detail. To get us started, a simple question: what exactly is the state of this Draft widget I made? Is it the state of the React component we designed in React Lesson 2 (and it’s parts)? Basically, what we wrote was an editor component—which is what I’ll call it from now on—and it had states shaped like this.

const initialState = {
    text : "",
    dirty: false,
    next: null,
    previous: null
}

If we want to persist our data to local storage, we need a clear idea of exactly what the app’s state is so we know what to persist.

I’m belaboring this point because, no, editor states like the initialState described above are not the state of the app. The purpose of this app is that it retains history. Not every previous editor state, but definitely the committed editor states, are all part of that history. Here’s what would not be acceptable: We let someone leave this page thinking we saved their work for them automatically. When they come back we show them the same text they had when they left, but all of their commit history is gone. Unacceptable. The whole purpose of the widget is to maintain this commit history. We have to save all the commits. That is the app state. (At least for now.)

In particular, right now the commits are stored in the form of a linked list. Right or wrong, the way I first thought of to write this makes each commit just a former editor state. So the state of the app has to contain this linked list and it must contain a pointer to exactly which element on the list is the current component state, and maybe some other things we haven’t thought of yet.

This states within states thing is kinda fun, right? And don’t you also feel like it’s going to get deeply confusing before it gets better?

We are not explicitly keeping track of this app state anywhere. It only exists implicitly as a linked list that lives in memory and a pointer to an element on it that lives only inside one of our components, plus the state of the editor—which is part of the app’s state. Storing the state implicitly is not necessarily a bad thing. Linked lists are very efficient. This setup works perfectly fine if you don’t need to grab and bottle the entire state for some reason. But if we want to store the entire state in local storage—or, say, a database on the backend—that is exactly what we have to do.

To properly make the app state explicit and store it to a local storage space, we might need to alter the application’s architecture. It raises the question: will we have to start storing the linked list of component states as JavaScript array? If we want to persist that information to local storage, then we have to project it into a JavaScript object—preferably something like a JSON-style object. The local storage API is a very simple dictionary of key-value pairs—both strings. It doesn’t store JavaScript objects. It stores strings. So data has to be stringified before it can be stored. Seems like a shame to turn our beautiful linked list into an array, but we may have to.

Before getting into the details of how to put the data in local storage, let’s focus on separating component state from app state. I’ve been staring at my reducer functions and actions and such for about an hour and I gotta say this is confusing as all hell. I think once we have figured out the WAY of Redux a bit more, it will all make sense, but I didn’t start this thing as an expert. You know what I do here. I write as I learn. To get there we have to drag our way through a swamp for a little while. It’s very common.

Here are some of my thoughts as I try to work out these distinctions between app-level state and editor-level state. Is “dirty” top-level, app-level state? Recall, “dirty” is an editor state value that keeps track of whether we are currently on an unaltered commit. If we are looking at either an unaltered initial workspace or an unaltered commit, then the form is not “dirty”. If you have changed anything since the most recent commit, or even added a space and then backspaced it, then the editor state is “dirty”. Is that top-level state, or does it only affect the editor?

Maybe it doesn’t matter and I’m over-complicating that question. But what exactly is a commit? A commit is not really a previous editor state. Editor states can be dirty, but a commit cannot be dirty. Commits have three meaningful properties: text, previous, and next. Text can be “”. Next and previous can both be null, or else they are other commits. But commits don’t have any such property as “dirty”. Commits are records of historical state. So to create a commit object from an editor state object, do we simply throw away the “dirty” property and push the resulting object onto an array?

I’m starting to understand some of this better. Designing the shape of the state of an app is basically the same thing as designing the schema of a database. Everything should be computable from state, but we should not have any duplication of information in state. Nota bene however: getting a handle on the state space and how it changes over difference user paths is a different matter. That is what the reducer function does in response to actions.

[Aside: I don’t know if anybody else would write the phrase “state space”, but I am a mathematician. If a mathematician says [something]-space, he often means the set of all possible values of that something. So state-space is an abstraction meaning the set of all possible states. It is an infinite-sized mathematical abstraction.]

Putting some of this together, for example, I realize that previously I was storing a lot of extra “dirty” properties and values for my commits even though that data was no longer relevant. That was a straight-up mistake I made. It was a minor one in this case, but only because the stakes are so small. In a different situation, storing a single extra bit of data per object could be a huge, huge performance drain for no reason. This didn’t occur to me before though, because before I was not making the proper distinction between a commit and a former editor state. I thought of them as the same thing.

Here is one conclusion I’ve come to so far. I’m changing the shape of what I called editor state. The editor needs to know three things: the current text input (from the user), the current commit (which is not necessarily the most recent one), and whether the form is dirty. Previous and former commits, if any, will be accessed in terms of the currentCommit reference.

Because the editor needs to know what the current commit is, that is where we store that piece of state data. This means the overall shape of my new app-level state object will be this:

// this is a schema, not a JS literal
state : State = {
  commits : Commit[]
  editorState : {
    text : string,
    currentCommit : Commit,
    dirty : boolean
}

Right now, I think that describes all the data one needs in order to describe any state of this app. Each object in the commit array will have this form.

// this is a schema, not a JS literal
myCommit: Commit = {
   text : string,
   previous : another commit or null,
   next : another commit or null
}

My reasoning is that all of the data accessible from the state.editorState object is enough for the editor component to render itself. And that is the minimum data it needs for its job. It does not need access to the list of commits. Am I going too far if I say it doesn’t even need access to all of the data in the current commit? It doesn’t actually need a reference to the previous commit. All it needs to know is whether there is one. I don’t think I’m going to far. Part of the beauty of organizing components into hierarchies is that you can control what data child components have access to. You pass a reference into a dumb object unnecessarily and later some other coder uses that reference to mutate the data.


Coders who are not you are always idiots who would mutate data you know should not be mutated.

Adam Cross

We are far from done here, but I don’t think I’ll make any useful progress unless I put this aside until tomorrow or later. As always, when I write a continuation I will edit this post and put a link right here.

[Update: Part 2 is here:
https://adamcross.blog/2019/04/29/states-within-states-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 )

Facebook photo

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

Connecting to %s