Adding the undo feature to React application from scratch is a fairly complex task. Managing state is already tricky, and implementing undo means having state versioning and tracking changes over time. Luckily, Immer.js makes it a lot simpler.
Let’s explore using Immer.js for managing complex states and handling undo while building a fun little dungeon crawler game.
What is Immer.js?
Immer.js is a wonderful library that has a wide range of applications. But its core use case is providing seamless immutability . It lets you modify data structures and returns copies instead of mutating the original.
You might be aware of other libraries that accomplish the same thing, mainly Immutable.js . The main reason Immer.js is a better option, among many others, is the fact that it works with native object types.
The biggest drawback of Immutable.js is that it provides a custom type for each data structure. Immer.js, on the other hand, returns the same object types it receives, which makes it a lot less intruding and easier to work with.
“produce” function of Immer.js
The main feature of Immer.js is the
function. The function accepts a source object and your custom handler for modifying the source.
produce returns a new copy while leaving the source intact:
Within the handler, you receive a draft parameter that you can use to mutate the source directly without the fear of side effects. The handler doesn’t have to return the draft. Immer.js is smart enough to track changes without the explicit return.
Immer uses the copy-on-write mechanism, which makes duplicating objects performant.
If you would like to learn more about Immer.js, the best place to start is their documentation .
Overview of patches
Building the undo feature involves using a more advanced functionality of Immer.js - patches .
Using patches, Immer.js keeps track of all the changes made to the state. The patches are JSON objects that follow a format Immer.js understands. Here’s what a patch looks like:
You can feed these patches back to Immer.js to reverse or reapply state changes using the
With the basic overview of Immer.js done, let’s get started with our tutorial.
As mentioned before, we will build a simple game. You are in a dungeon and have three doors in front of you. One of them has the treasure you seek, while the other two have deadly enemies behind them.
The most important feature of the game (for us) is that you can cheat using the undo button.
We will use useReducer to manage the state of the application. Here’s the initial setup of our reducer:
Our reducer returns a new state using the
produce function. All the state manipulation happens inside of the produce handler. Notice again how we can freely mutate the state, and everything still works. Pretty cool!
generateDoors returns an array of doors with randomized content.
We use our reducer the same way we normally would:
Now lets use patches.
produce function actually can accept three parameters:
- Initial source object. In our case, it’s the state of the reducer.
- Handler function for state mutation.
- A second handler that receives patches. We will need to store these patches for later use.
The patch handler function receives two parameters:
- patches - an array of patches with the latest state changes.
- inversePatches - an array of patches for reverting the latest state changes.
In our case, we only care about the
inversePatches array. However, if you were to implement the redo, you would want to keep track of
Let’s provide the handler that will record inverse patches as they arrive:
Note that we need to call
enablePatches to activate the patches feature.
We check to make sure we only track patches for undoable actions. In our case,
OPEN_DOOR is the only undoable action.
Also, when resetting the game, we want to clear patches in the
The final step here is adding the
UNDO action that applies the inverse patches we store:
We pop the latest inverse patch from the
inverseChanges array and apply it. Note that when calling
applyPatches, Immer.js requires us to explicitly return the result. More on that
That’s it for this tutorial. We saw how easy it is to add the undo feature to React with Immer.js.
Immer.js is a great immutability library, and patches allow for creating some advanced functionality. You can use patches for building features like communicating updates over WebSockets, tracing/debugging, versioning, and more.
Here’s a link to the codesandbox with the full code of this tutorial.