React useState with History
Tags: discuss, react, javascript, typescript
Published: 2022-06-02 12:13:18

I recently came across a post about a React Hook that keeps track of past states, something like _useState with History_. In addition to the current state and the state update function, it returns an array of all states (previous states plus current state) as the third parameter. The implementation looked like this:

```ts
function useStateHistory<T>(
  initialValue?: T | (() => T)
): [T | undefined, (state: T) => void, Array<T | undefined>] {
  const stateHistoryRef = React.useRef<Array<T | undefined>>([]);
  const [state, setState] = React.useState<T | undefined>(initialValue);

  React.useEffect(() => {
    stateHistoryRef.current = [...stateHistoryRef.current, state];
  }, [state]);

  return [state, setState, stateHistoryRef.current];
}
```

First of all, I like this implementation because it is simple and easy to grasp what is going on. The `useState` hook manages the current state, the `useEffect` hook reacts to changes in the current state and stores each state change in an array of states defined by the `useRef` hook. 

However, thinking about it a more deeply, the `useEffect` hook is actually redundant and can be omitted if we convert `useState` into a `useReducer` which allows us to define a reducer function to update the state. Inside the reducer we can simply store the new state into the array of `useRef`. 

```ts
const stateHistoryRef = React.useRef<Array<T | undefined>>([]);
const [state, setState] = React.useReducer(
  (oldState: T | undefined, newState: T | undefined) => {
    stateHistoryRef.current = [...stateHistoryRef.current, oldState];
    return newState;
  },
  typeof initialValue === "function"
    ? (initialValue as () => T)()
    : initialValue
);
```

However, there is a caveat with this implementation. React actually calls the state reducer function _twice_. This behavior is [intentional](https://github.com/facebook/react/issues/16295#issuecomment-610098654) to make unexpected side effects more apparent. The reducer function should be pure, i.e. it should return the same output for the same input and should not have any side effects, such as changing the value of a ref within the reducer.

To make the reducer pure, we need to remove the `useRef` and manage the state history within the reducer function itself. This means that instead of returning a single state, the `useReducer` will return an array of all states and take care of merging the old state with the new one. In our `useStateHistory` hook we then simply take the last element of the state history array and return it as the current state, the remaining states are the history. 

```ts
function useStateHistory<T>(
  initialValue?: T | (() => T)
): [T | undefined, (state: T) => void, Array<T>] {
  const [allStates, setState] = React.useReducer(
    (oldState: T[], newState: T) => {
      return [...oldState, newState];
    },
    typeof initialValue === "function"
      ? [(initialValue as () => T)()]
      : initialValue !== undefined
      ? [initialValue as T]
      : []
  );

  const currentState = allStates[allStates.length - 1];
  const stateHistory = allStates.slice(0, allStates.length - 1);
  return [currentState, setState, stateHistory];
}
``` 

To be honest, these changes are minuscule and I don't expect them to improve any performance. I just like to think about the objective and how it can be achieved in a different way, in this case with only one hook instead of three. I assembled an example on CodeSandbox to compare the different implementations of the hooks.

{% embed https://codesandbox.io/embed/usestatehistory-ztkpbe?fontsize=14&hidenavigation=1&theme=dark %}

What is your opinion on this? Would you rather use more hooks and have a simple implementation, or use as few hooks as possible with a possibly more complicated implementation?