Check out a free preview of the full State Management in Pure React, v2 course:
The "Implementing Undo & Redo" Lesson is part of the full, State Management in Pure React, v2 course featured in this preview video. Here's what you'd learn in this lesson:

Steve demonstrates how to keep track of all past, present, and future states.

Get Unlimited Access Now

Transcript from the "Implementing Undo & Redo" Lesson

[00:00:00]
>> One of the things that we get from kinda having reducers allows it, we saw that we could, effectively create an abstraction over to listen for actions and then dispatch later things right. We can also start to do more complicated state management patterns with a reducer, that we could ever do with use state.

[00:00:20] And so one of them is, if we wanted to implement effectively what we might call time travelling, the ability to undo and redo. And effectively, it's just going to be two stacks, right? The hardest part of all of this will be understanding time travel. And so the time of this recording, it's been seven months since Avengers Endgame came out, so I will be following the time traveling rules of Endgame.

[00:00:50] Spoiler alert, this time traveling in Endgame, also Darth Vader's Luke Skywalker's father. Bomber, right? So we are going to follow the the timeline which is you can go back in time, but if you make any changes that branches off into a new time line, okay? Again, the hardest parts of this will unfortunately be just wrapping your head around time travel.

[00:01:12] I am like, DM me on Twitter. I'm happy to talk about this way more than you're comfortable with, so we can figure it out. So we are following Avengers Endgame time traveling rules. And we'll get into the code, but at the end of the day, we're simply gonna, instead of having that list of grudges, right?

[00:01:34] What we're gonna do is, we're gonna have the list of grudges, we are also going to keep track of all previous states, right, cuz we're always dispatching an action. We're not necessarily setting the state by hand like we were doing with used state, we're announcing that a thing happened, right?

[00:01:51] And so when we announce that a thing happened with dispatch, what we're gonna do is, we're gonna take the present state of the world, we're gonna put it on to an array of all previous states. Then we're going to create the new state of the world in our reducer, all right?

[00:02:04] And use that. If someone says undo, we take the present, we move it into the future, because it's now a new future. And we take the most recent past, we move it to the present. If they do something from the past and that creates a new branch of the future.

[00:02:18] Again, Avengers Endgame rules. Like we can get real complicated with this and then you end up as your own grandfather. It's not interesting. So we'll have a past, a present and a future. Past and future are arrays, present is the present state of the world. Basically present is what we currently have in our reducer.

[00:02:36] And we'll just keep track of past and present states of the world. All right, so go back into grudge bin, or grudge list, or whatever I'm calling it these days. And we'll see that we can just start to refactor this a little bit. And we'll start as our kind of our initial state.

[00:02:59] All right, our initial state is gonna change, instead of this one that we pass and we'll take the one that happens. And we go into the grudge context, you can see that the one we pass in here. Where is that user? There it is. This initial state. We're going to take the dummy data that we have, but that's only the present initial state.

[00:03:20] That is not all previous timelines. So what we'll do is we'll say, I don't know, we'll call it defaultState. And that will be past. The present is gonna be that initialState. And then the future is a series of, yeah, future states, assuming we don't mess with the timeline.

[00:03:49] Feel very strongly about this. And will pass it in there, cool. So now we've got, our reducer's effectively broken, right? Because state is now state.present, Right? So we need to fix that. And to be really clear, I've done this enough times to know that as I am live coding, I will mess up.

[00:04:10] So if you see like, didn't he forget a dot present? He did, just tell him, right? [LAUGH] It's not that he's got some crazy thing that he's about to do. He made a boo boo [LAUGH] right? Cool, so we'll start out by just we've got the state, we'll just give it a default state again.

[00:04:32] So now the action type is GRUDGE.ADD, we need to return. We'll start out just by saying cut that and we'll say present. And this will be state.present. So we're just kind of realizing it's gone one layer deeper, right? All the previous what we thought about is the only only timeline is now present.

[00:04:57] And then here we'll say state. I'm actually just gonna pull this out to make it a little bit cleaner for us. I'll say. Const newPresent. We'll say, state.present.map and then we'll return. Past, present is that newPresent future Is an array. And then finally, getting the actual component. This is gonna be the full state and the grudges.

[00:05:49] Actually it's gonna be. Grudges will be state.present. All right, let's see if I broke anything, shall we? Let's actually make sure that's spun up. All right, they load, I can forgive. I can say hello to Word.
>> [LAUGH]
>> Live-using an app that you've coded is also fraught with peril.

[00:06:33] Cuz basically, we've just moved everything down, right? We haven't actually implemented any of this yet. Here, we can also say, let's go ahead and do the same pattern that we did before, here. We'll actually pull this out to const newPresent. And we'll say, past. So sorry, we'll implement the time traveling in a second.

[00:07:15] Maybe we'll even include a comma. Cool, everything still works. We don't have any time traveling yet. We've just made everything slightly more complicated. So that's cool. All right, so we're almost all of the way there, strangely. All we need to do now is whenever the present changes, we need to add what was the present to the list of past worlds.

[00:07:48] So we can do that. And we'll say, so we've got the new present. We'll just say that the past is anything that was in the past, cuz there could be stuff in there. But before that, we'll say. State.present, so the past is now what was the present and anything that was already in the past.

[00:08:09] There's some Kylo Ren quote in there. We'll do the same thing here, as well. Cool, so I'll have that. Now, we probably love to have some way to actually go into the past, right? So we'll do that and we'll create another one here called, just an action creator and this is a really simple one.

[00:08:48] And it will be. A function that's just gonna dispatch an action of the type, UNDO. I will parse that in as well. And we're using use Callback here, just to not create a brand new one. But if you're asking, I should do that, right? We could just be a function too.

[00:09:24] This would make a new function every time. I'm mostly doing it to keep it in my, we know that the contacts API broke most of our caching, but I'm using it just for consistency at this point. But all it does is dispatch this type of undo. Now, it's just conditional logic in the reducer.

[00:09:39] So if you can dispatch any kinda action you want and if the reducer doesn't know about it, it'll just flow through, right? So we'll put that in there and then in application. Or actually, let's do it in. Yeah, we'll do it here. We'll say. We'll undo and then we probably.

[00:10:07] Let's actually also just parse in all of the state, in this case, just to keep it simple for ourselves. We could refactor later. We probably won't, but we could. So we're back into GrudgeContext. I'm also gonna, you have the grudges. Which is now gonna be grudges is. We just make it, grudges is going to be state.present by the way.

[00:10:31] So grudges. Got undo. Cool. Feel like we have a problem cuz I feel like it shouldn't work right now. So we've got a grudge in the present. I also wanna know, is there a past at all, right? Because basically we just wanna disable the undo button if there's no past to undo and is there a future.

[00:11:11] So we could say something like const isPast and that will be state.past.length. And really if it is zero than it is false, right? If there's nothing in the past, then we don't want it and if there is something, then we do. So if there is a past, just turn that into a Boolean.

[00:11:39] Then const is future. Cool, so we've got those in place as well. Cuz we want disable to be true, it might make sense to do the opposite here, so we'll say no past. That feels weird, we'll flip it in. It feels not semantic. So we'll pass those into the context as well.

[00:12:09] We'll pass in that undo action. We'll say is past, or we'll code is there a past, but we didn't. So is there a past and is there a future? And application, I'm just gonna put in here for now to make sure it looks right. I'm gonna do like a.

[00:12:34] Hold > Undo and let's just check that out. It's a little gross, but it'll work. A little button called undo and we'll have another one called redo.