Why React 18 effect calling twice is a good thing

4 min read  •  25 Jun 2022
Views:

If you're reading this, I am assuming that you already know that with React 18 Strict Mode useEffect is being called twice in development mode. If you haven't saw it yet, below is a sample for it, we can see that Hello React 18 log is coming twice in the console.

twice-effect

Now seeing this behaviour there might be multiple questions, why is it getting called twice? and that too only in development? and not in production?


Let's try to understand this with an example, and make a small app which implements following things:

  • has an input number (userId) which can be incremented or decremented
  • everytime userId changes, api call is made to get and store userName
  • display both userId and userName

Now to make this work, I have made one mock function api.fetchData which takes userId and returns userName. If userId is even, it will return response after 300ms or else it will return after 600ms. I have kept the response return time variable since, api requests are async tasks and they can return response after variable time period depending upon network speed. Here is the code for that:

const api = {
  fetchData(id) {
    return new Promise((resolve) => {
      let wait = id % 2 == 0 ? 300 : 600;
      setTimeout(() => {
        resolve(`User ${id}`);
      }, wait);
    });
  },
};

Now, let's implement the above requirements:

export default function App() {
  const [userId, setUserId] = useState(0);
  const [userName, setUserName] = useState("");

  useEffect(() => {
    setUserName("");
    api.fetchData(userId).then((data) => {
      setUserName(data);
    });
  }, [userId]);

  return (
    <div>
      <input
        type="number"
        defaultValue={0}
        placeholder="User Id"
        onChange={(e) => {
          setUserId(e.target.valueAsNumber);
        }}
      />
      <h3>Id: {userId}</h3>
      {userName && <h3>User: {userName}</h3>}
    </div>
  );
}

problem with above approach

The above solution works, but if you have looked closely there is something wrong. If we try to change userId quickly (eg: from 4 to 6), what happens is we get response for userId 6 after 300ms and for userId 5 after 600ms, and eventually the userName of userId 5 gets set even though our current userId is 6.

We can see this below-

without-cleanup

We can see the problem in the above example, i.e. even when the userId 5 component instance was unmounted but it was still setting the data. These kinds of issues may arise if we don't maintain the cleanup function.

According to React Docs, "If your Effect fetches something, the cleanup function should either abort the fetch or ignore its result"


let's fix this problem by adding a cleanup function

We can create a variable (done) and initialize it as false and on unmount we can change its value to true, and we can check value of done before updating the state.

export default function App() {
  const [userId, setUserId] = useState(0);
  const [userName, setUserName] = useState("");

  useEffect(() => {
    let done = false;

    const fetchAndSetData = (id) => {
      setUserName("");

      api.fetchData(id).then((data) => {
        !done && setUserName(data);
      });
    };

    fetchAndSetData(userId);

    return () => {
      done = true;
    };
  }, [userId]);

  return (
    <div>
      <input
        type="number"
        defaultValue={0}
        placeholder="User Id"
        onChange={(e) => {
          setUserId(e.target.valueAsNumber);
        }}
      />
      <h3>Id: {userId}</h3>
      {userName && <h3>User: {userName}</h3>}
    </div>
  );
}

And, let's check how it solves our problem:

with-cleanup

But why does React runs useEffect(fn, []) twice

With the above example, we can see the importance of cleanup function in our react code. Same is helpful when we are dealing with connection, subscriptions, event listeners etc... but often times devs can miss to handle such conditions. To help the developer notice such issues, React 18 (StrictMode) remounts the component once after initial mount (in development only), which helps in identifying such issues and further fix them by adding required cleanup function.


References