It's a running joke at Signavio that the product I'm working on does not support undo and redo. And even though we built the tool with redux we're still struggling to get that functionality in. I'm not going to bore you with the details of why it's hard for us but over the years(!) I got an idea of why it might be hard, in general, to get features like undo-redo into an existing app. The good news is that I believe there are steps you can take to make it easier.
TL;DR
. Check it out on GitHub. 😄However, this post is more about architecture than it is about undo-redo. The topic is a good example to showcase how certain choices for your architecture can help you built new features faster. Also, you'll see how small decisions can have large impacts. So, go ahead and learn about:
- The principles behind undo-redo,
- that your components should not care about how they get their data, and
- that some concerns are better handled in isolation.
The basics of undo and redof(state, action) => state*
then you might have encountered this as
f(state, action) => state*
Every time something happens inside our app we create a new present state. By doing this we also disregard any information about what came before. It becomes clear that to undo an action we'll need to keep track of our past and also our future (that's redo).
The most basic implementation I know to built undo-redo is to introduce two stacks next to the present state. One stack keeps track of what happened before and the other stack tracks what would happen next. I like to use stacks because they best fit my mental model. This way we're coming from the past, move over to the present, and into the future. You could also use arrays and always append to the end but this would make popping items just that tiny bit harder.
With the above data structure in place, we need to establish some ground rules for how we want to work with it.
Progressing the present
Whenever an action happens that is neither "undo" nor "redo" we need to first move the current present into the past and then create a new present which is our updated application state.
With every user action, we also need to clear the future
stack.
That's mostly to save ourselves from having some bad headaches.
If we would not do that then with every "redo" we would need to figure out which of our possible futures we're going to choose.
Everyone who has ever watched Back to the future knows that you don't want to do that.
By the way, I'm not talking about redux actions or reducers here. When I'm talking about actions in this article I mean "the user has done something and our application state is somehow updated". It's important to understand this to not get tricked into thinking undo-redo can solely be built with a redux based architecture.
Undo
You might have guessed already what is going to happen when we undo an action. When this happens we're going to take the most recent item that's in the past stack and make it our present. Also, we're pushing the current present into the future stack (that's important for redo).
Redo
When we "redo" an action we're making the immediate future our current present and the current present becomes the past again. Are you still with me? This is usually the point where I've made sure I've lost everyone in the room.
Now that I've confused you let's move on to the actual topic of this blog post: the effect this has on your application architecture.