Oliver Roick

usePrevious Implementations in Comparison

Subtle differences in implementation can lead to bugs on your application.

There are rare cases where you need access to the previous value of a state variable. usePrevious hooks allow us to store and access the previous value of a state. There are different implementations, not all created equal.

The UI framework Chakra includes an undocumented usePrevious implementation that looks like this:

export function usePrevious<T>(value: T) {
  const ref = useRef<T | undefined>()
  useEffect(() => {
    ref.current = value
  }, [value])
  return ref.current as T
}

The previous value is stored in a ref and the initial value is set in an effect hook on first render. Initially, the previous value will be undefined even if the hook is initialised with a value. On top of that, by using a ref instead of a state, the previous value will be equal to the current value in any subsequent re-renders of the parent component.

The usePrevious implementation of the useHooks library has a similar problem.

export function usePrevious(value) {
  const [current, setCurrent] = React.useState(value);
  const [previous, setPrevious] = React.useState(null);

  if (value !== current) {
    setPrevious(current);
    setCurrent(value);
  }

  return previous;
}

It stores the previous value in an internal state, which solves the problem related to Chakra’s use of ref to store the previous value. But previous is initialised with null, a value the provided state never held.

Both approaches are wrong. We rely on usePrevious to understand what the previous value of a state was. If a state is initialised with a value, the initial previous value should only be undefined or null if the hook was initialised with undefined or null, otherwise it should be initialised with the provided value.

If we are trying to track specific value transitions of the state, say we want to execute a routine only if the state changed from undefined or null to something else, both implementations will cause bugs.

The correct way is to initialise the previous value with the initial value provided to the hook.

function usePrevious(value) {
  const [current, setCurrent] = useState(value);
  const [previous, setPrevious] = useState(value);

  if (value !== current) {
    setPrevious(current);
    setCurrent(value);
  }

  return previous;
}