how do you prevent functions inside useEffect from being triggered more than once?

Viewed 319

There is an ESLint rule exhaustive-deps which checks the dependency array of your Hooks (ex:useEffect). My question lies around the impact of having the content of the useEffect be triggered more often due to things like functions now being tracked.
This can have a negative impact when an api call is inside an effect because you would only want the API to be called once.
I could break up the Effect more, and maybe it helps with the dep array...maybe not...

How do you prevent content from being triggered more than its needed? For me, in order to prevent the extra calls to API service, I either need to ignore the lint rule (no!) or have more checks in place around my code, as well as have local state about the status:

Example to prevent the session & api call from being triggered more than once:

useEffect(() => {
    if (!saveValues) {  // is this an anti-pattern?
      return;
    }
    if (!sessionId && !wasSaving && savingForm) {
      initSession().then(() => { // 1st call to session service to get Id
        setSendRequest(true); // set local state that it's safe to send API request because I have my session ready
      });
    } else if (sendRequest && sessionId) {
      setSendRequest(false); // immediately set local state back to false so the previous block doesnt fire.
      try {
        const fields = userFields
          .filter(field => !isEmpty(saveValues[field.name]))
          .map(field => {
              name: field.name,
              value,
            };
          );
        insertUser({ sessionId, fields, config, newRoles }) // here's my API call now
          .then(() => saveSession(sessionId)) // 2nd call to session service req'd
      } catch (err) {
        const message = err.message.split('desc = ')[1];
        setSaveRequestError(message);
        setSavingForm(false);
        setSaveModalOpen(false);
        quitSession(sessionId);
      }
    }
    return () => {
      if (sessionId) {
        quitSession(sessionId);
      }
    };
  }, [
    initSession,
    insertUser,
    quitSession,
    roles.rolesList,
    saveSession,
    saveValues,
    savingForm,
    sendRequest,
    sessionId,
    userFields,
    wasSaving,
  ]);

as a side note, interestingly, the hooks for state, like setSaveRequestError, setSavingForm do not show up in dep array...

4 Answers

Due to react documentation here and here it is better to put each of your conditions to a separate useEffect instead of a single complex one. Each useEffect has its own dependency array and do specific job.

If you just want to avoid executing it again (and you don't want to pass an empty dep array) useRef is your friend since it does not rerender the component (not a state):

let hasBeenExecuted = useRef(false);
//In effect
if(!hasBeenExecuted.current) {
    // Do stuff
    hasBeenExecuted.current = true
}

I am sorry for being late. This repository is to solve this problem.

react-use-watch

useWatch(() => {
  // your logic
}, [your dependency])

Solve this problem neatly and perfectly.

useEffect gets called every time a dependency passed to it changes and runs the entire code inside it.

Note: useEffect(() => {}, [dependency1, dependency2 ...])

If you want your code inside the useEffect to be called only when the component loads initially, pass empty array as dependency.

Related