Day 187: Undo to Undo
I was going to write yet another lengthy technical post about normalizing data and why it’s not always such a good idea, but I exercised outside a few hours ago and my allergies are kicking my butt so I’m going to try to keep this short.
(Update: Okay, somehow ended up super long anyway.)
If you read through the Redux documentation, you’ll find that they strongly recommend that you normalize your data. Here’s a blurb from the basic tutorial:

Normalizing your data means breaking up each type of data into its own “table” in the state and referencing things by ID rather than storing items within other items.
For example, what the blurb above is talking about is that instead of using a structure like
{
todos: [{
text: "write road to ramen post",
completed: false
}, {
text: "tell family you love them",
completed: true
}]
}
you’d want to organize your data to look like
{
todos: ["todo1", "todo2"],
todosById: {
todo1: {
text: "write road to ramen post",
completed: false,
},
todo2: {
text: "tell family you love them",
completed: true
}
}
}
Some of the benefits of normalizing your data:
- If an item is duplicated in multiple places (e.g. a todo item is included in multiple lists), you only need to update the item once for it to be changed everywhere
- You avoid unnecessary re-renders. If I change the text of
todo1and my data were normalized, only thetodosById > todo1part of the tree will change so only components that care about that section would re-render. If my data weren’t normalized, the identity of the entiretodosarray would change since state is immutable in Redux, meaning that every todo would re-render.
After spending countless hours reading the Redux docs this past week, I got the message: NORMALIZE YOUR DATA.
So when it came time to migrate the way I manage commands within flows, I knew that The Redux Way would be to normalize the data. That is, instead of flows holding commands directly, they should reference them by ids instead.
Not normalized:
{
flows: {
flow1: {
commands: [{
type: "SCRIPT",
code: "console.log('hello world')"
}, {
type: "CLICK",
selector: "a:nth-child(1)"
}],
}
}
}
Normalized:
{
flows: {
flow1: {
commands: ["command1", "commmand2"]
}
},
commands: {
command1: {
type: "SCRIPT",
code: "console.log('hello world')"
},
command2: {
type: "CLICK",
selector: "a:nth-child(1)"
}
}
}
Since commands aren’t shared between flows, I wouldn’t get any benefits in terms of avoiding issues with duplicate data, but I’d still benefit from avoiding unnecessary re-renders. I didn’t think it really mattered since flows are generally pretty small so re-rendering wasn’t going to be a bottleneck, but I figured I’d be a good little developer and follow The Redux Way.
So I spent all of yesterday refactoring my code to normalize commands and get everything working again.
This morning, I was excited to FINALLY take a break from the Redux migration (there’s more to do, but there’s enough done that I can do some feature work) and implement one of the features I was most excited to get for free by using Redux: undo/redo history.
I could’ve implement it from scratch and this guide explains how to do so really well, but there’s a library called redux-undo that already handles everything. In brief, the way that it works is by storing the previous state of different slices of your Redux state. So if I add a command to an empty flow, the flow’s state would look like this:
// Other fields omitted for brevity
{
past: [{
commandIds: []
}],
present: {
commandIds: ["command1"],
},
future: []
}
And then if I added another command, it’d look like:
{
past: [{
commandIds: []
}, {
commandIds: ["command1"]
}],
present: {
commandIds: ["command1", "command2"],
},
future: []
}
What would the flow’s state look like if I edited a command?
Well, the contents of the command would change but the reference would stay the same, so…
The flow’s state wouldn’t change at all. The command’s state would.
Cue sinking feeling.
If someone clicks “undo”, I want them to undo whatever action they took on the flow: reordering a command, editing a command, adding a new command, etc.
But in order to undo editing a command, I’d need to go find that command’s state and look at that command’s history in order to figure out what the previous state was.
In order for the undo functionality to work, I couldn’t just drop in redux-undo on my flow data and have it work.
Because I normalized my data.
At this point, I sauntered back into my room and laid in bed while I asked myself where my life had gone so wrong. Clearly, the powers that be were punishing me for some grave sin I had committed.
I spent the next hour repeating that cycle: Going back to my desk, googling “redux-undo normalized data” a million times in the hopes that there was some solution that I wasn’t thinking of, losing hope with every StackOverflow question that had nothing to do with my issue whatsoever, going back to my room to lie in bed in despair, and resisting the realization that was becoming clearer every minute: I needed to denormalize the data and put the commands back into the flow.
If I had stuck with my general practice of keeping my commits tiny, this wouldn’t have been that big of a deal — sure, I’d lose a day’s worth of work, but I wouldn’t lose more time than that. A few git reverts would do the trick.
Unluckily for present me, past me had been so confident in the decision to normalize my commands data that I had also made related-but-not-necessary changes in the same mega commit as the refactor. So if I tried to revert it, I’d lose a bunch of other work that I’d done. Rookie mistake.
After looping through git revert, “oh man, maybe it’d be easier to just do it manually”, git reset --hard, starting to edit a bunch of different files, “oh man, doing it manually is brutal” and using git revert again, I eventually sucked it up and manually made the changes. Thankfully, once I put my mind to it, It didn’t suck as much as I thought it would.
Anyway, I got through it, I added redux-undo to my non-normalized data, and now….
CHECK IT OUT YO
YEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
I mean this is some really basic Redux stuff but I’M STOKED
Lessons learned:
- Best practices aren’t always best in every situation. CHECK YOSELF B4 U WRECK YOSELF
- Redux is dope and I’m now a fanboi (ty shiva 🙏🏽)
- At the same time, I’m so over this migration
- Wait, why am I even adding undo/redo? I should be focusing on shipping