react: Bug: useEffect runs twice on component mount (StrictMode, NODE_ENV=development)
React version: 18.0.x, 18.1.x, 18.2.x
Steps To Reproduce
- Visit provided sandbox
- Open console and observe logs displayed twice.
- Click the button and observe the rendering log happens twice, the effect log happens once.
Link to code example: https://codesandbox.io/s/react-18-use-effect-bug-iqn1fx
The current behavior
The useEffect callback runs twice for initial render, probably because the component renders twice. After state change the component renders twice but the effect runs once.
The expected behavior
I should not see different number of renders in dev and prod modes.
Extras
The code to reproduce:
import { useEffect, useReducer } from "react";
import "./styles.css";
export default function App() {
const [enabled, toggle] = useReducer((x) => !x, false);
useEffect(() => {
console.log(
"You will see this log twice for dev mode, once after state change - double effect call"
);
}, [enabled]);
console.log("You will see this log twice for dev mode - double rendering");
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<button onClick={() => toggle()}>
Toggle me: {enabled ? "on" : "off"}
</button>
</div>
);
}
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Reactions: 2
- Comments: 41 (2 by maintainers)
The “best” advice depends on how your app is built:
This advice is not new and is not specific to React 18, but maybe we haven’t shared it very clearly before. I’ve written a longer and more nuanced version in this Reddit reply. I hope this clarifies the intention behind sharing this advice a bit better.
If you fetch from effects, you can do something like this:
This will not prevent the double-fetch, but it will ignore the result of the first one. So it’s like it never happened. There is no harm from the extra fetch call in development. You could also use a fetching helper with cancellation, and cancel the fetch instead of ignoring its result. Keep in mind that in production you’ll only get one fetch.
Read more:
So basically, in React 18 is a better idea install a third party library to do API fetching.
For API fetching, if we do not wish to include another third party dependency, where exactly should we do the fetching if not in use effect?
The official docs at https://reactjs.org/docs/faq-ajax.html say componentDidMount (Which, I guess shows that it hasn’t been updated in a while). Wasn’t componentDidMount equivalent to a use effect with empty dependencies?
I assume that the
StrictModedoes more than that. And it wasn’t doing that in React 17. I don’t argue - I’m just curious why this specific breaking behaviour couldn’t have been added as opt-in. I assume there are good reasons behind that.Anyway - I don’t need more explanations - will just stick to React 17 for a while longer.
To give you more context: I used to teach React classes + I know a bunch of people who do it too. I can imagine their confusion when they try to explain useEffect during some live coding session with CRA-bootstrapped project and the code is not behaving as they expect. I wouldn’t stress about it if it was explicitly stated in the docs of useEffect.
Also I find it very common in recruitment coding assignments from the category: “show me you can fetch some data from the REST API”. I can imagine both the recruiter and the candidate being surprised why the fetch is being called twice (assuming they work with React 17 projects right now and used CRA just for the sake of the live coding task).
Thank you @eps1lon for clarification. However this is not intuitive and I’d expect this being mentioned/referenced in the useEffect docs here: https://reactjs.org/docs/hooks-effect.html
I can imagine people being surprised by this behaviour. The perfect solution would be to have a warning in the console.
Hi, react-query maintainer here. 👋
glad to see people are happy with react-query. 🙌 just for completeness, I’d like to point out that even in react-query, the strict effects fires the fetch “twice”. We just deduplicate multiple requests that are going on at the same time, so it literally doesn’t matter if one component mounts, then unmounts, then mounts again, or if 5 consumers call useQuery at the same time.
strict-effects have also shown us two edge-case bugs already, so I’m really glad that exists. 🙏
react-query supports
AbortSignals, so if that is used, what will happen is that the first fetch is aborted and the second fetch will supersede the first one. This might also throw people off because there are two requests in the network tab, but it’s how aborting queries is designed to work 😃It is described in the upgrade post. We strongly recommend anyone upgrading to read the full upgrade post.
https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#updates-to-strict-mode
I agree it’s confusing this isn’t documented explicitly in the conceptual introduction to effects.
We’ll fix this in the new docs.We’ve published some docs on this now:I disagree about the console log. The DevTools log is different because it disappears by installing DevTools. So most users (who we presume install DevTools) won’t get it.
Thank you for that @gaearon 😃
Any hints where to put
fetchcalls so that they are not triggered twice in dev modeuseEffectcallback?Currently the thread focuses almost completely on fetching, but there is another side of the story: behavior of complex components: https://github.com/facebook/react/issues/24553
What worries me is the fact that development mode most people will run (StrictMode is there in CRA) is getting further from production mode. So area for making subtle errors that disappear as soon as you start troubleshooting is increasing.
For posterity:
Just out of curiosity I went and check how
react-querysolves this problem and it’s impressive how many things they handle out of the box.To prove the problem doesn’t occur I created this sandbox: https://codesandbox.io/s/react-query-vs-react-18-strict-mode-xpzkr1
Once again thanks for explaining the problem @gaearon
Case closed 😉
Effects firing twice in
<React.StrictMode />was added in React 18.For more information, check out StrictMode: Ensuring reusable state
The mental model for the behavior is “what should happen if the user goes to page A, quickly goes to page B, and then switches to page A again”. That’s what Strict Mode dev-only remounting verifies.
If your code has a race condition when called like this, it will also have a race condition when the user actually switches between A, B, and A quickly. If you don’t abort requests or ignore their results, you’ll also get duplicate/conflicting results in the A -> B -> A scenario.
Is it only me or anyone also has this doubt - why the ignore variable will not be reinitialised to false when component is unmounted and mounted again on initial render in development?
@gaearon could you put some light on this…
Just use something like this:
We mostly use this pattern for doing redirects and pages with side-effects, like log-out.
rendering twice is not the same as fetching twice. if you look at the network tab, there is only one request going out. strict mode still renders the component twice, and since v18, it does not silence the console on the second run, so you will see double logs.
Yeah, that’s not ideal! But it does offer a method for prefetching and priming the cache. It also has support for server rendering.
The
ignorevariable is just there to avoidsetResult(rest)if the component is unmounted before the promise is fulfilled. As @gaearon said this variable will NOT preventfetchfrom being called twice, but prevents the first one setting the result while React is unmounting and remounting for the second time. The secondfetchthough perhaps manages to callsetResultas no unmount would happen afterwards.I tried to ask around regarding the difference between
fetch-on-renderandrender-as-you-fetch. The details still seem pretty fuzzy to me, but react-query is well equipped to handle both, with or without suspense.render-as-you-fetch seems to be just a fancy phrase for saying: trigger the fetch manually before you render the component, then react can render the rest in the meantime. This is what
queryClient.prefetchQueryoffers. At that point, bothSuspenseandErrorBoundariesbasically just offer a way to have more global loading spinners / error handlings - and both concepts are supported in react-query, independently of each other 😃As far as I can see, even with render-as-you-fetch, if you don’t prefetch and read from two different resources in the same component tree, you will wind up with a waterfall.
@gaearon That’s really clever, however I’d expect something like that being an opt-in feature of strict mode anyway. It’s a surprising behaviour for the components that by design are not remounted during my app lifecycle (eg. some root-level context providers). It’s just a personal opinion though. I can imagine turning it on/off by some extra prop. However I see that for the simplicity it’s much simpler to just let it be there.
Yeah, if you check network tab, you can see that the request goes only once as mentioned here. The console logs shows the subsequent re-renders that React 18 brings in.
So suppose, even if a user goes like A -> B -> A, react-query still handles it; it does not fetch twice.
If you want to fetch before you render, you can use prefetch client, as explained here; or fetch server side. React Query supports both @moinulmoin
I checked your sandbox link. It’s still rendering twice. @cytrowski Can you please describe how React Query solves this issue? is it fetching data once?
Yep!
No, they’re not the same. In my example,
ignoreis local to that specific call of the effect. It doesn’t affect other calls. In your example, the ref affects all effect calls.@TkDodo I’ve seen that it’s not just some clever hack around
useEffect- instead it’s nicely deduped 😃 Thanks for making that clear 😉@cytrowski https://github.com/facebook/react/blob/main/CHANGELOG.md#react-1 <- In the “stricter strict mode” bullet