80

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;
Youssouf Oumar
  • 29,373
  • 11
  • 46
  • 65
hossein fti
  • 900
  • 1
  • 6
  • 12
  • 2
    Does this answer your question? [React 18 strict mode causing component to render twice](https://stackoverflow.com/questions/72489140/react-18-strict-mode-causing-component-to-render-twice) – neil Sep 22 '22 at 12:47

5 Answers5

142

useEffect being called twice on mount is normal since React 18 when you are in development with StrictMode. Here is an overview of what they say in the documentation:

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 documentation:

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, and I would suggest you read the article as a whole.

Youssouf Oumar
  • 29,373
  • 11
  • 46
  • 65
  • 1
    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? – Steven Lacks Dec 07 '22 at 15:36
  • 1
    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](https://beta.reactjs.org/learn/synchronizing-with-effects), where they talk about [Not an Effect: Buying a product](https://beta.reactjs.org/learn/synchronizing-with-effects#not-an-effect-buying-a-product) and [Sending analytics](https://beta.reactjs.org/learn/synchronizing-with-effects#sending-analytics), and pretty much every use case. – Youssouf Oumar Dec 07 '22 at 15:50
  • @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? – avepr Dec 13 '22 at 00:45
  • This will be hard to debug @avepr, as there is no enough code and context. – Youssouf Oumar Dec 13 '22 at 11:48
  • 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. – dan Jan 05 '23 at 09:10
  • 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. – Youssouf Oumar Jan 05 '23 at 09:28
  • @dan Isn't that what cancel tokens for network requests are for? – Drew Reese Jan 10 '23 at 09:43
  • 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. – cst1992 Apr 25 '23 at 05:35
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);
  };
}
dubble
  • 89
  • 1
  • 5
  • Usually, 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. – Youssouf Oumar Dec 02 '22 at 12:45
  • 1
    @yousoumar I completely agree and would not recommend this anymore. – dubble Dec 03 '22 at 01:52
4

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.

dan
  • 1,545
  • 3
  • 20
  • 29
  • 1
    Hi @dan! Thanks for completing the thread, but your solution won't work in production, as the logic after the `if` won't run at all if the component doesn't re-render at least once (and there is nothing that ensures that). Even if it does, it won't change anything because of the empty dependency array. At this point, in production, it's like you don't have this `useEffect`. Usually, we shouldn't use those technics and prefer optimizing` useEffect` with clean-ups, request caching... – Youssouf Oumar Dec 02 '22 at 10:06
  • Hi @yousoumar, thank you for your comment. But, (IMO) there can be scenarios where this approach can be still useful. – dan Jul 06 '23 at 05:03
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.

Ahmed Sbai
  • 10,695
  • 9
  • 19
  • 38
  • Should we have a data update as a side effect? React team doesn't seem to agree, as you can read on [Not an Effect: Buying a product](https://beta.reactjs.org/learn/synchronizing-with-effects#not-an-effect-buying-a-product). *"The right question isn’t “how to run an Effect once”, but “how to fix my Effect so that it works after remounting”."* – Youssouf Oumar Mar 14 '23 at 07:08
  • In the implementation I have, there is no direct event that triggers this, it is like a listner, I don't think that using a ref harms in development or it is ? – Ahmed Sbai Mar 14 '23 at 07:16
  • As you said, this effect won't run in production. But if it does, this won't prevent your effect from being called multiple times in production, one per the user navigating to another page and coming back unless the effect is in a global component that won't ever be removed from the screen. – Youssouf Oumar Mar 14 '23 at 07:19
  • 1
    Yes it is a global component – Ahmed Sbai Mar 14 '23 at 07:21
  • Both ways dont work, even using useRef, it will initialize the count again and it still runs twice – Techie_T Sep 01 '23 at 10:17
-2

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