-
Notifications
You must be signed in to change notification settings - Fork 46.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bug: useEffect runs twice on component mount (StrictMode, NODE_ENV=development) #24502
Comments
Effects firing twice in For more information, check out StrictMode: Ensuring reusable state |
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. |
Also: I see no mention of this breaking change in the changelog: https://github.com/facebook/react/blob/main/CHANGELOG.md#1800-march-29-2022 |
@cytrowski https://github.com/facebook/react/blob/main/CHANGELOG.md#react-1 <- In the "stricter strict mode" bullet |
@sean-clayton this does not explain why I see the rendering log twice after state change (not only initial render). However I believe there is already an issue for that. Anyway I still believe this should be something announced in the console warnings (similar way you do with encouraging React Dev Tools installation) |
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. 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 |
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: useEffect(() => {
let ignore = false;
fetchStuff().then(res => {
if (!ignore) setResult(res)
})
return () => { ignore = true }
}, []) 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: |
For posterity: Just out of curiosity I went and check how 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 ;) |
So basically, in React 18 is a better idea install a third party library to do API fetching. |
Hi, react-query maintainer here. 👋 glad to see people are happy with react-query. 🙌 strict-effects have also shown us two edge-case bugs already, so I'm really glad that exists. 🙏
react-query supports |
@TkDodo I've seen that it's not just some clever hack around |
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. |
@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. |
This is exactly what |
But is this Edit: |
@yangmingshan This is not a ref, just a local variable specific to that particular effect. Rather than to the component lifetime like a ref. |
But if I change it to ref: const isUnmountedRef = useRef(false)
useEffect(() => {
isUnmountedRef.current = false
fetchStuff().then(res => {
if (!isUnmountedRef.current) setResult(res)
})
return () => { isUnmountedRef.current = true }
}, []) Are they same? |
No, they're not the same. In my example, |
I assume that the 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). |
Would you like to send a PR? I totally agree the docs should mention this. |
I think I will find some time this week @gaearon :) Thanks - will do the PR. Should I tag it with this issue number? |
Yep! |
It took me less than one week 😉 Hope those 2 paragraphs are enough to make everyone happy 😄 reactjs/react.dev#4650 |
But react-query also uses fetch-on-render approach which is not good as you mentioned |
Yeah, that’s not ideal! But it does offer a method for prefetching and priming the cache. It also has support for server rendering. |
I tried to ask around regarding the difference between 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 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. |
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? |
@gaearon What would you suggest to do otherwise? I know about libraries like react-query which does not fetch in useEffect, but is there any other solution (without using any third-party library)? Thanks |
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. |
Just use something like this: import { type EffectCallback, useEffect, useRef } from 'react';
export const useMount = (effect: EffectCallback) => {
const mounted = useRef(false);
useEffect(() => {
if (mounted.current) {
return effect();
}
mounted.current = true;
return () => {};
}, [mounted, effect]);
}; We mostly use this pattern for doing redirects and pages with side-effects, like log-out. |
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? |
useEffect(() => {
let ignore = false;
fetchStuff().then(res => {
if (!ignore) setResult(res)
})
return () => { ignore = true }
}, []) 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... |
Please how do you use it? |
I bet you can just const MyFancyComponent = () => {
useMount(() => console.log('The component is mounted'));
return <div>Hello</div>
} or if you want to do something on "unmount": const MyFancyComponent = () => {
useMount(() => {
console.log('The component is mounted');
return () => console.log('The component unmounts');
});
return <div>Hello</div>
} |
The |
@motevallian thanks for this awesome explaination |
Currently the thread focuses almost completely on fetching, but there is another side of the story: behavior of complex components: #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. |
Yeah, if you check network tab, you can see that the request goes only once as mentioned here. 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 |
The second log is there because React 17+ batches all the logs in dev mode and prints them again at the end. Open this sandbox in separate tab and see the difference in browser console. Codesandbox does not distinct between those two - the browser console does. |
We've documented this in detail on the Beta website. I've also responded on Reddit to a question about how to fetch data in React 18 with some historical context. Hope this is helpful. Sorry some links from there are 404 because other pages are still being written. I'll lock this thread since we have a canonical answer and I don't want it to get lost in the follow-up comments. But if there's some pattern the new doc doesn't cover please let us know in a new issue. |
React version: 18.0.x, 18.1.x, 18.2.x
Steps To Reproduce
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:
The text was updated successfully, but these errors were encountered: