States within States (Part 2)

This is a continuation of States Within States (Part 1). The source code for this project is hosted here: https://github.com/xerocross/xero.draft .

Ultimately, I decided on a simplified and flattened schema for my states like this.

// schema
const initState = {
    commits : Commit[],
    currentCommitIndex : number,
    currentText : string,
    dirty: boolean
}

This does mean I changed the architecture of the app a little. A Commit is nothing but a simple immutable wrapper for a text string. Now the commits array is the source of truth for notions like the previous commit and the next commit (if any). I liked using a linked list for that structure, but it did not lend itself to my needs anymore.

I’ve taken careful measures in my new reducer function to make sure state is never mutated. For example, every time we return a new state, the commits array is a brand new array. I do not clone each individual Commit element, but those elements are immutable. Once a commit is constructed, there is no way to alter the text content.

I must say I strongly agree with Clean Code author Robert C. Martin right now when he advises that having a robust testing suite allows one to confidently make changes to code. Because I have written a robust battery of unit tests, I was able to completely change the architecture of this app and still remain confident that it functions correctly.

Inside my editor component now, I have code like this.

this.store.subscribe(() => {
    let state = this.store.getState();
    this.setState(()=> {
        let existsNext = state.currentCommitIndex < state.commits.length - 1;
        let existsPrevious = state.currentCommitIndex > 0;
        return {
            text : state.currentText,
            dirty : state.dirty,
            existsNext : existsNext,
            existsPrevious : existsPrevious
        }
    })
});

This function is what takes the app state from our store and transforms it into the component’s local state. But I do wonder about things like, say, what would happen if we grab the state object from the store and explicitly do something stupid like set state.commits = []. It appears that Redux does not automatically include some safeguard against that kind of thing. I just ran a little experiment where I added state.commits = [] at the top of my subscribe callback above. It caused all sorts of errors, but it didn’t throw any sort of explicit “invalid state mutation” error. Obviously I don’t plan to do anything that stupid, and this is a one-man project for now. But what if it wasn’t? How do I safeguard against some other person who does not really understand how state is supposed to work?

Modern JavaScript does offer the method Object.freeze which may be the way to go here. Back in my reducer function, now I have changed it so that the returned state is always frozen first. This keeps us from altering the value of state.commits to a different reference, but the state.commits array itself is still mutable. We can freeze the array too by doing something like this: newState.commits = Object.freeze(newState.commits) before we freeze the newState object.

Having made those changes, I see that my tests still pass. Also, if I intentionally write in some stupid code that attempts to mutate my state object, it throws an explicit error about mutating being illegal. This puts us in a good position to commit the architecture updates and merge into master.

Next I’m going to use this new architecture to make it so changes to state automatically get persisted to local storage. That will be another post. I’ll link to it here when it is done.

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