3

I understand that React calls useEffect twice in strict mode, this question is about asking what the correct way of handling it is.

I have recently hit an issue with React useEffect being called twice in strictMode. I'd like to keep strict mode to avoid issues, but I don't see any good way of making sure some specific effects are only run once. This is in a next.js environment, where I specifically want the code to only run in the client once.

For example, given the following component:

import React, { useState, useEffect } from "react";
import ky from "ky";

const NeedsToRunOnce: React.FC = () => {
  const [loading, setLoading] = useState(false);

  const doSomethingThatOnlyShouldHappenOnce = (data: any) => {
    // Do something with the loaded data that should only happen once
    console.log(`This log appears twice!`, data);
  };

  useEffect(() => {
    if (loading) return;
    console.log("This happens twice despite trying to set loading to true");
    setLoading(true);
    const fetchData = async () => {
      const data = await ky.get("/data/json_data_2000.json").json();
      setLoading(false);
      doSomethingThatOnlyShouldHappenOnce(data);
    };
    fetchData();
  }, []);
  return <div></div>;
};

export default NeedsToRunOnce;

useEffect will be called twice, and both times loading will still be false. This is because strictMode makes it be called twice with the same state. The request will go through twice, and call doSomethingThatOnlyShouldHappenOnce twice. All the console.log calls above will appear twice in the console.

Since I can't modify the state to let my component know that the request has already started happening, how can I stop the get request from happening twice and then calling code that I only want to be called once? (For context, I am initialising an external library with the data loaded in the useEffect, and this library should only be initialised once).

I found a GitHub issue about this where Dan Abramov says:

Usually you’d want to have some kind of cleanup for your effect. It should either cancel your fetch or make sure you ignore its result by setting a variable inside your effect. Once you do this, there should be no actual difference in behavior.

If you cancel your fetch in effect cleanup, only one request will complete in development. However, it also doesn’t matter if another request fires. It’s being ignored anyway, and the stress-testing only happens in development.

While I agree in principle, this is tedious in practice. I've already hit two different use cases in my application where I have some library call or request that needs to only be done once in the client.

What's a reliable way of making sure that a piece of code in useEffect is only run once here? Using a state to keep track of it having already been run doesn't help, since react calls the component twice with the same state in a row.

nialna2
  • 2,056
  • 2
  • 25
  • 33
  • 1
    Your effect's dependency array is empty, but your effect reads `loading`, so it must also depend on it. – AKX May 11 '22 at 16:50

1 Answers1

3

You can use a ref to execute your useEffect once (first time or second time, as you wish), or just use a customHook. In my case, i execute the useEffect the second time. Swap true and false, to execute it the first time and not the second one.

import { useEffect, useRef } from "react";

export default function useEffectOnce(fn: () => void) {
  const ref = useRef(false);
  useEffect(() => {
    if (ref.current) {
      fn();
    }
    return () => {
      ref.current = true;
    };
  }, [fn]);
}

And to use it, you can pass your callback function in param :

useEffectOnce(() => console.log("hello"));
Forth
  • 114
  • 1
  • 6
  • We shouldn't use those technics and prefer optimizing useEffect with clean-ups, request caching... As the behavior is there for a good reason, we should deal with it correctly instead of avoiding it. For a more detailed answer, you can visit https://stackoverflow.com/questions/72238175/useeffect-is-running-twice-on-mount-in-react – Youssouf Oumar Dec 03 '22 at 09:01