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

Time:05-21

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.

CodePudding user response:

Those effects which should be run only once can be handled using useRef.

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;
};

More reading.

CodePudding user response:

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