What is the correct way to initiate a side-effect on page load when using React Strict Mode?

Viewed 114

When using Strict Mode useEffect is always invoked at least twice, i.e.

useEffect(() => {
  console.log('Hello, World!');
}, []);

The above code will print "Hello, World!" "Hello, World!".

This breaks how I have traditionally implemented , e.g. log out upon visiting /log-out page:

export const LogOutPage = () => {
  const { logout } = useAuth();

  useEffect(() => {
    logout({
      onCompleted: () => {
        console.log('logged out');
      },
      variables: {},
    });
  }, []);

  return null;
};

useEffect will be invoked twice and so is onCompleted, causing "logged out" to be logged twice.

I thought this could be solved by using state to prevent useEffect from being called the second time...

export const LogOutPage = () => {
  const [loggedOut, setLoggedOut] = useState(false);

  const { logout } = useAuth();

  useEffect(() => {
    if (loggedOut) {
      return;
    }

    console.log('loggedOut: ', loggedOut);

    setLoggedOut(true);

    logout({
      onCompleted: () => {
        console.log('logged out');
      },
      variables: {},
    });
  }, [loggedOut]);

  return null;
};

However, because the initial value of loggedOut is false, it still executes that code path twice, i.e. it will wring "loggedOut: false" "loggedOut: false"

It would appear that the only way to prevent this is to trigger the desired side-effect by a change in state, i.e.

export const LogOutPage = () => {
  const [startedLogOut, setStartedLogOut] = useState(false);
  const [loggedOut, setLoggedOut] = useState(false);

  const { logout } = useAuth();

  useEffect(() => {
    // This will be invoked twice.
    // However, the operation is idempotent.
    setStartedLogOut(true);
  }, []);

  useEffect(() => {
    if (!startedLogOut) {
      return;
    }

    setLoggedOut(true);

    if (loggedOut) {
      return;
    }

    logout({
      onCompleted: () => {
        console.log('logged out');
      },
      variables: {},
    });
  }, [startedLogOut, loggedOut]);

  return null;
};

This indeed works, however, it feels convoluted, and it is not an pattern I've observed in the wild. Asking this question to confirm that this is how I should be implementing side-effects when using Strict Mode.

2 Answers

There is a section in react docs about that, I suggest you read that. There is also one option which at some point was mentioned on github page of react, and it is shown below. You can use this as last resort.

export default const LogOutPage = () => {
  const [loggedOut, setLoggedOut] = useRef(false);

  const { logout } = useAuth();

  useEffect(() => {
    if (loggedOut.current) {
      return;
    }

    console.log('loggedOut: ', loggedOut.current);
 
    loggedOut.current = true;

    logout({
      onCompleted: () => {
        console.log('logged out');
      },
      variables: {},
    });
  }, []);

  return null;
};
import { type EffectCallback, useEffect, useState, useRef } from 'react';

/**
 * @see https://stackoverflow.com/a/72319081/368691
 */
export const useMount = (effect: EffectCallback) => {
  const ref = useRef<ReturnType<EffectCallback>>(undefined);

  const [mounted, setMounted] = useState<boolean>(false);
  const [invoked, setInvoked] = useState<boolean>(false);

  useEffect(() => {
    setMounted(true);
  }, [setMounted]);

  useEffect(() => {
    if (!mounted) {
      return () => {};
    }

    setInvoked(true);

    if (invoked) {
      return ref.current;
    }

    ref.current = effect();

    return undefined;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [setInvoked, invoked, mounted]);
};

This implementation:

  • correctly invokes the effect on the first mount
  • correctly invokes clean
  • works in production and Strict Mode
Related