Creating Memory-Leak Proof Reducer With React Hooks

Memory Leaks Caused by Async Actions in React

All fancy React applications we create use asynchronous code, like http requests. Asynchronous side effects which updates component state may cause memory leaks. I'm pretty sure most of you got an error saying:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

What this error indicates is that your async job has finished after your component is unmounted and you did some state changes depending on that job. That means you got a memory-leak in your application. But don't be frustrated, because it's easily fixable.

We're going to build a custom hook just for this purpose which uses React's useReducer hook to manage the state. But you can implement this logic to useState hook, or any hook that manages state.

In order to make "safe" state updates we need to know if the component is mounted or not. There's a really straightforward way to do this actually. Which uses useRef and useEffect (or useLayoutEffect) hooks.

function SomeFancyComponent() {
  ...
  const mountedRef = React.useRef(false);

  React.useEffect(() => {
    mountedRef.current = true; // we assign mountedRef's value to true when the component is mounted
    return () => mountedRef.current = false; // this cleanup function runs when the component is unmounted
  }, []); // empty dependency array means this effect only runs when the component is mounted
  ...
}

This code-block makes sure that mountedRef.current is true only if the component is mounted. By using this logic we can make sure we don't update the state when the component is not mounted.

Creating useSafeReducer Hook

Let's use the logic from above to implement our own reducer hook, which is memory-leak proof.

function useSafeReducer(...args) {
  const [state, unsafeDispatch] = React.useReducer(...args);
  const mountedRef = React.useRef(false);

  React.useEffect(() => {
    mountedRef.current = true;
    return () => {
      mountedRef.current = false;
    }
  }, []);

  const dispatch = React.useCallback((...args) => {
    if(mountedRef.current) {
      unsafeDispatch(...args);
    }
  }, []);

  return [state, dispatch];
}

Tada! We have a reducer that only makes state changes when component is mounted.