React Hook to Allow Undo/Redo
Tags: javascript, react, webdev, beginners
Published: 2021-11-17 07:19:30

If you're looking to build no-code tools like [FormBlob](https://formblob.com), one _must-have_ feature is the ability to undo and redo actions. Why? Imagine you were working on an image-editing software and you made multiple changes to your canvas. After some time, you realise that what you had before looked much better than what you have now. You would undo your way back until you get to a stage you're satisfied with.

Now, if the software did not have an undo/redo function, you would most probably unleash some flowery language and abandon the software forever.

So how do we implement an undo/redo function and prevent users from abandoning our app?

### Prerequisite

If you're not familiar with React Hooks, I suggest you read up about them [here](https://reactjs.org/docs/hooks-intro.html). One of the most fundamental hooks is React's built-in useState hook. This allows you to store component state within a variable and manage it as required. In this tutorial, we will be writing a hook that extends the useState hook to allow for undo/redo functionality.

### The Code

Let's start with the code and then I'll explain below.

```jsx
import { useMemo, useState } from "react";
// If you're only working with primitives, this is not required
import isEqual from "lodash/isEqual";

export default function useUndoableState(init) {
  const [states, setStates] = useState([init]); // Used to store history of all states
  const [index, setIndex] = useState(0); // Index of current state within `states`

  const state = useMemo(() => states[index], [states, index]); // Current state

  const setState = (value) => {
    // Use lodash isEqual to check for deep equality
    // If state has not changed, return to avoid triggering a re-render
    if (isEqual(state, value)) {
      return;
    }
    const copy = states.slice(0, index + 1); // This removes all future (redo) states after current index
    copy.push(value);
    setStates(copy);
    setIndex(copy.length - 1);
  };

  // Clear all state history
  const resetState = (init) => {
    setIndex(0);
    setStates([init]);
  };

  // Allows you to go back (undo) N steps
  const goBack = (steps = 1) => {
    setIndex(Math.max(0, Number(index) - (Number(steps) || 1)));
  };

  // Allows you to go forward (redo) N steps
  const goForward = (steps = 1) => {
    setIndex(Math.min(states.length - 1, Number(index) + (Number(steps) || 1)));
  };

  return {
    state,
    setState,
    resetState,
    index,
    lastIndex: states.length - 1,
    goBack,
    goForward,
  };
}
```

### Concept

As with useState, useUndoableState accepts only 1 argument, the initial value. Behind the scenes, the hook uses two main variables to determine state - `index` (number) and `states` (array). `states` stores the historical values of the state while `index` determines current state by indicating the current position in the array.

You may navigate through historical states by using the `goBack` and `goForward` functions emitted by the hook. However, if you make a call to `setState` and `index` is not at the end of the `states` array, all states after `index` is erased and `index` will go back to the end of the `states` array. In other words, once you call `setState`, you can no longer redo.

The following table attempts to provide a more detailed explanation of the object returned by the hook:

| Prop       | Type     | Usage               | Description                                                                                                           |
| ---------- | -------- | ------------------- | --------------------------------------------------------------------------------------------------------------------- |
| state      | `any`    |                     | Current state, initialised with argument passed                                                                       |
| setState   | `func`   | `setState(value)`   | Sets state to `value`. All values after current `index` is erased                                                     |
| resetState | `func`   | `resetState(value)` | Deletes historical states and resets to value                                                                         |
| index      | `number` |                     | The current index in the `states` array                                                                               |
| lastIndex  | `number` |                     | The last index in the `states` array. Can be used to determine if can `goForward`. `canGoForward = index < lastIndex` |
| goBack     | `func`   | `goBack(2)`         | Goes back the number of steps passed                                                                                  |
| goForward  | `func`   | `goForward(3)`      | Goes forward the number of steps passed                                                                               |

### Usage

```jsx
import React from "react";
import useUndoableState from "path/to/hook";

const init = { text: "The quick brown fox jumps over the lazy dog" };

export default function Document() {
  const {
    state: doc,
    setState: setDoc,
    resetState: resetDoc,
    index: docStateIndex,
    lastIndex: docStateLastIndex,
    goBack: undoDoc,
    goForward: redoDoc
  } = useUndoableState(init);

  const canUndo = docStateIndex > 0;
  const canRedo = docStateIndex < docStateLastIndex;

  return (
    <div style={{ display: "block" }}>
      <textarea
        style={{ margin: "16px" }}
        onChange={(event) => setDoc({ text: event.target.value })}
        rows="5"
        value={doc.text}
      />
      <div>
        <button
          onClick={() => undoDoc()}
          disabled={!canUndo}
          style={{ marginRight: "8px" }}
        >
          Undo
        </button>
        <button
          onClick={() => redoDoc()}
          disabled={!canRedo}
          style={{ marginRight: "8px" }}
        >
          Redo
        </button>
        <button onClick={() => resetDoc(init)}>Reset</button>
      </div>
    </div>
  );
}
```

{% codesandbox use-undoable-state-2spts %}

### Concluding Remarks

With undo/redo functionality, [FormBlob](https://formblob.com) is one of a select few no-code form builders that gives you the flexibility to edit your forms without the fear of losing a previous state. As a no-code tool, FormBlob allows anyone to build and publish stunning forms and surveys in 2 minutes. [Try it free today!](https://build.formblob.com/builder)