Why useEffect running twice and how to handle it well in React?
Asked Answered
M

6

114

I have a counter and a console.log() in an useEffect to log every change in my state, but the useEffect is getting called two times on mount. I am using React 18. Here is a CodeSandbox of my project and the code below:

import  { useState, useEffect } from "react";

const Counter = () => {
  const [count, setCount] = useState(5);

  useEffect(() => {
    console.log("rendered", count);
  }, [count]);

  return (
    <div>
      <h1> Counter </h1>
      <div> {count} </div>
      <button onClick={() => setCount(count + 1)}> click to increase </button>
    </div>
  );
};

export default Counter;
Maximin answered 14/5, 2022 at 7:26 Comment(0)
L
202

useEffect being called twice on mount is normal since React version 18 when you are in development with StrictMode. Here is an overview of the reason from the doc:

In the future, we’d like to add a feature that allows React to add and remove sections of the UI while preserving state. For example, when a user tabs away from a screen and back, React should be able to immediately show the previous screen. To do this, React will support remounting trees using the same component state used before unmounting.

This feature will give React better performance out-of-the-box, but requires components to be resilient to effects being mounted and destroyed multiple times. Most effects will work without any changes, but some effects do not properly clean up subscriptions in the destroy callback, or implicitly assume they are only mounted or destroyed once.

To help surface these issues, React 18 introduces a new development-only check to Strict Mode. This new check will automatically unmount and remount every component, whenever a component mounts for the first time, restoring the previous state on the second mount.

This only applies to development mode, production behavior is unchanged.

It seems weird, but in the end, it's so we write better React code, bug-free, aligned with current guidelines, and compatible with future versions, by caching HTTP requests, and using the cleanup function whenever having two calls is an issue. Here is an example:

/* Having a setInterval inside an useEffect: */

import { useEffect, useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => setCount((count) => count + 1), 1000);

    /* 
       Make sure I clear the interval when the component is unmounted,
       otherwise, I get weird behavior with StrictMode, 
       helps prevent memory leak issues.
    */
    return () => clearInterval(id);
  }, []);

  return <div>{count}</div>;
};

export default Counter;

In this very detailed article, React team explains useEffect as never before and says about an example:

This illustrates that if remounting breaks the logic of your application, this usually uncovers existing bugs. From the user’s perspective, visiting a page shouldn’t be different from visiting it, clicking a link, and then pressing Back. React verifies that your components don’t break this principle by remounting them once in development.

For your specific use case, you can leave it as it's without any concern. And you shouldn't try to use those technics with useRef and if statements in useEffect to make it fire once, or remove StrictMode, because as you can read on the doc:

React intentionally remounts your components in development to help you find bugs. The right question isn’t “how to run an Effect once”, but “how to fix my Effect so that it works after remounting”.

Usually, the answer is to implement the cleanup function. The cleanup function should stop or undo whatever the Effect was doing. The rule of thumb is that the user shouldn’t be able to distinguish between the Effect running once (as in production) and a setup → cleanup → setup sequence (as you’d see in development).

/* As a second example, an API call inside an useEffect with fetch: */

useEffect(() => {
  const abortController = new AbortController();

  const fetchUser = async () => {
    try {
      const res = await fetch("/api/user/", {
        signal: abortController.signal,
      });
      const data = await res.json();
    } catch (error) {
      // ℹ️: The error name is "CanceledError" for Axios.
      if (error.name !== "AbortError") {
        /* Logic for non-aborted error handling goes here. */
      }
    }
  };

  fetchUser();

  /* 
    Abort the request as it isn't needed anymore, the component being 
    unmounted. It helps avoid, among other things, the well-known "can't
    perform a React state update on an unmounted component" warning.
  */
  return () => abortController.abort();
}, []);

You can’t “undo” a network request that already happened, but your cleanup function should ensure that the fetch that’s not relevant anymore does not keep affecting your application.

In development, you will see two fetches in the Network tab. There is nothing wrong with that. With the approach above, the first Effect will immediately get cleaned... So even though there is an extra request, it won’t affect the state thanks to the abort.

In production, there will only be one request. If the second request in development is bothering you, the best approach is to use a solution that deduplicates requests and caches their responses between components:

function TodoList() {
  const todos = useSomeDataFetchingLibraryWithCache(`/api/user/${userId}/todos`);
  // ...

And if you are still having issues, maybe you are using useEffect where you shouldn't be in the first place, as they say on Not an Effect: Initializing the application , and Not an Effect: Buying a product. I would suggest you read the article as a whole.

Liegnitz answered 14/5, 2022 at 7:37 Comment(10)
Every where I look, the assumption is that the request in the useEffect is a GET or that we didn't clean up a subscription. We want to update a resource on unmount (PATCH). Any thoughts?Heartthrob
Hi Steven! I'm not sure updating a resource in an useEffect is a good idea. Anyway, if your use case is not covered, I suggest you read Synchronizing with Effects, where they talk about Not an Effect: Buying a product and Sending analytics, and pretty much every use case.Liegnitz
@yousoumar This all good and vanilla ice cream. But what do I do if I have a paginated list of items and I append a next page of items to existing list that I store in a state? Say I have a state: const [items, setItems] = useState([]) and whenever I get new page from API I use useEffect(()=> { setItems(current => [...current, ...page])}, [page]); This seems to be proper code but in Strict Mode it will double the list by concatenating twice. Any solution to that?Cappadocia
This will be hard to debug @avepr, as there is no enough code and context.Liegnitz
Hi @yousoumar, what if the given request is responsible for changing the state of the server itself - in that case can we take the statement You can’t “undo” a network request that already happened lightly? Say, for example, a request to refresh-user-token is being called twice.Cortico
Hi @dan, it's hard to say, but for anything where you cannot have two calls, a useSomeDataFetchingLibraryWithACache is relevant. Because even though in production it doesn't get called twice by React on mount, the user can, by changing the page he is at, and coming back, for example.Liegnitz
@Cortico Isn't that what cancel tokens for network requests are for?Asleyaslope
If you have a setInterval(), it's not enough to just wire its cleanup to unmount as in certain cases both timers are not cleared. In my case I'm storing the timer id in state so that I can clear it later. But the dual mount throws away the old timer id and it keeps running without me being able to do anything, even after the component is unmounted.Tragedian
I do understand why they designed this, but it would way better to offer an config file to disable this functionality in strict mode. I understand every detail in React and don't need this while still want to keep strict mode on.Higley
I DON'T think enabling Strict Mode by DEFAULT is a GOOD idea. I use SMS verification to verify the legitimacy of requests, but the results are always alternating between success and failure. It wasn't until I discovered that React defaults to executing the function in useEffect twice, even when there are no changes in the dependencies!Towhead
C
28

While I agree with the points raised in the accepted answer,

If one still needs to use useEffect

Then, using the useRef() to control the flow is an option.

To apply the effect ONLY on the FIRST mount:

const effectRan = useRef(false);

useEffect(() => {
  if (!effectRan.current) {
    console.log("effect applied - only on the FIRST mount");
  }

  return () => effectRan.current = true;
}, []);

To apply the effect on the REmount:

const effectRan = useRef(false);

useEffect(() => {
  if (effectRan.current || process.env.NODE_ENV !== "development") {
    console.log("effect applied - on the REmount");
  }

  return () => effectRan.current = true;
}, []);

When is this useful?

One application is where the useEffect contains a server request that can potentially change the state of the backend (e.g. DB change). In that case, unintended (duplicate) server requests due to StrictMode can lead to unforeseen results.

Can't we cancel the request via AbortController?

Yes, we can cancel a request. However, by the time the cancel gets invoked, the request might have already run to completion, if not altered number of backend states. Abort does not guarantee a sound rest, at least not out of the box. Therefore, (I think) a request that is NOT intended; should not be attempted in the very first place.

Cortico answered 29/11, 2022 at 5:42 Comment(1)
Fantastic answer, and yes - that's the only solution that has worked for me despite implementing exactly like said in the documentation. Only useRef stopped the double fetchingContamination
H
4

Update: Looking back at this post, slightly wiser, please do not do this.

Use a ref or make a custom hook without one.

export const useClassicEffect = createClassicEffectHook();

function createClassicEffectHook() {
  if (import.meta.env.PROD) return React.useEffect;

  return (effect: React.EffectCallback, deps?: React.DependencyList) => {
    React.useEffect(() => {
      let isMounted = true;
      let unmount: void | (() => void);

      queueMicrotask(() => {
        if (isMounted) unmount = effect();
      });

      return () => {
        isMounted = false;
        unmount?.();
      };
    }, deps);
  };
}
Hemimorphite answered 25/6, 2022 at 4:9 Comment(1)
Can you explain some of whats going on in this code? What is createClassicEffectHook and queueMicroTask? I think that would help with understanding the solution.Shaman
A
3

I had a use case in which useEffect runs to persist data in the database using next-auth provider "so I don't really need the purpose of strict mode it in this case", and even AbortController() didn't help

You can’t “undo” a network request that already happened

So the data is persisted twice, I had to fix it this way, using ref :

const count = useRef(0);
useEffect(() => {
  if (count.current !== 0) {
    // code
  }
  count.current++;
}, [])

This will make the code inside useEffect only running the second time.

NOTE: do not use this in production since there is no strict mode there, so the code inside useEffect will not run at all


update:

As it is mentioned in @YoussoufOumar's comment below

you shouldn’t put a logic that persist into database in an Effect but instead you should put it inside a function that runs when an event is happening ( button clicked for example ) otherwise the code will be executed each time the component is mounted.

However, in my case, there is no such an event the component works like a listner and it mounts only once.

Athematic answered 14/3, 2023 at 7:0 Comment(1)
Both ways dont work, even using useRef, it will initialize the count again and it still runs twiceKalman
V
0

This is what I have been using to get around this issue. Yes it will force an extra state change, but I would rather use this simpler solution to avoid having to cancel the use effects I only need to call once (api or otherwise) when trying to debug other problems.

I had a more complex solution that was canceling request and trying to detect the "fake" dev only call but it was causing errors on some of my pages that made it harder to debug in general so using the KISS rule here. (even more so because of all the possible 'async'ness with this problem)

export const useEffectOnce = ( effect )=> {
    const [needToCall, setNeedToCall] = React.useState(false);

    React.useEffect(()=> {
        if (needToCall) {
            effect();
        }
        else {
            setNeedToCall(true);
        }
    }, [needToCall]);
};

I just dropped this method in my common util file and call it as you would a normal useEffect as before

useEffectOnce(() => {
  // your code here you want to run once even in strict dev
})
Ventura answered 5/7, 2023 at 4:40 Comment(0)
K
0

you can add this hook to run it only once:

import { DependencyList, EffectCallback, useEffect, useRef } from "react";

const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
  const mounted = useRef(false);

  useEffect(() => {
    if (mounted.current) {
      return cb();
    }
    mounted.current = true;
  }, dependencies);
};

export default useEffectAfterMount;

and use it:

  useEffectAfterMount(() => {
   ...
  }, []);
Kajdan answered 24/1 at 9:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.