-
Notifications
You must be signed in to change notification settings - Fork 7.6k
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
[Suggestion]: StrictMode confusion about ordering of the events #7315
Comments
@Tosheen, good day! useEffect in StrictMode works a little differently. In StrictMode, the React does mount, unmount, and remount the component again, but useEffect is called at another stage of the React algorithm and it has "its own life", however, it is also called, immediately cleared and called again. This is done in order to properly track the moments when some logic was performed in useEffect, but the user has already left the page where this component was. For example: import { useEffect } from "react";
export default function App() {
useEffect(() => {
const onChange = () => console.log("resize", onChange)
window.addEventListener("resize", onChange)
}, [])
return <div>test</div>
} In this code, we subscribe to the "resize" event, but we don't unsubscribe from it anywhere, and StrictMode, thanks to this "trick", points us to this problem before we could notice it. To solve this problem, we need to modify the following code useEffect(() => {
const onChange = () => console.log("resize", onChange)
window.addEventListener("resize", onChange)
return () => {
window.removeEventListener("resize", onChange)
}
}, []) This also applies to http requests that are sent to useEffect. Strict Mode will call requests twice after all stages of useEffect processing, but this is done for a reason, since there are often cases when the request was sent, but the user has already left the page, the data has returned - but the component is no longer there. To avoid such problems, StrictMode will handle useEffect separately and point out this error by calling the request twice. If you ignore this warning, in the case that I described above, the following problem may appear: To solve this problem, you should use data processing after calling fetch or use an AbortController In conclusion, I want to say that StrictMode also works correctly, and in general does not violate its concept of "first mount" -> "unmount component" -> "second mount", just useEffect is called at another stage of processing the algorithm, which is why with StrictMode, useEffect processing goes at the very end, and with "manual rewire" - in the middle of the process Hope this helps! |
Hi, @pamellix. |
@branko-toshin-deel To be honest, I do not know why the StrictMode logic is implemented in this way, but I assume that this is done in order to optimize the application in dev mode. useEffect starts executing between the second and third stages of React rendering, when the basic manipulations with vDOM and DOM have already been performed. I assume this was done with the intention of not overloading StrictMode's work with unnecessary operations. I think that with StrictMode React, when rendering for the first time, it only works with what changes the DOM, skipping the stages of working with effects. And after the second render, it already fully processes all the effects that are in the component, thereby reducing the load. Roughly speaking, if I understand everything correctly, with StrictMode we have a "one and a half" render (that is, the first render with skipping effects, and the second render with processing effects twice) of the component, and with "manual re-render" - all two re-renders. I also think that the React team did not introduce such a detail into the documentation because the developers pay little attention to it, more precisely, when working with StrictMode, this detail should not ruin the operation of the application, but I will try to make my contribution to the StrictMode documentation in order to describe in detail its logic of operation! P.S. I also assume that this is done so that working with StrictMode feels not just like "two renderers", but like "one, but an extended render". That is, roughly speaking, StrictMode works with each stage separately, and does not just re-render components twice, thereby it retains the primary logic of the React render, with all stages. I guess StrictMode tests the component in stages not the way we think (I use numbers to indicate the stages of rendering)
it tests them like this
(just my guess) |
@pamellix |
@branko-toshin-deel |
@pamellix That would be most appreciated. |
@branko-toshin-deel I'm sorry for such a long break. After looking at the React code, I found the methods responsible for calling the effects twice. They are located here: function doubleInvokeEffectsOnFiber(
root: FiberRoot,
fiber: Fiber,
shouldDoubleInvokePassiveEffects: boolean = true,
) {
setIsStrictModeForDevtools(true);
try {
disappearLayoutEffects(fiber);
if (shouldDoubleInvokePassiveEffects) {
disconnectPassiveEffect(fiber);
}
reappearLayoutEffects(root, fiber.alternate, fiber, false);
if (shouldDoubleInvokePassiveEffects) {
reconnectPassiveEffects(root, fiber, NoLanes, null, false);
}
} finally {
setIsStrictModeForDevtools(false);
}
} and: function doubleInvokeEffectsInDEVIfNecessary(
root: FiberRoot,
fiber: Fiber,
parentIsInStrictMode: boolean,
) {
const isStrictModeFiber = fiber.type === REACT_STRICT_MODE_TYPE;
const isInStrictMode = parentIsInStrictMode || isStrictModeFiber;
// First case: the fiber **is not** of type OffscreenComponent. No
// special rules apply to double invoking effects.
if (fiber.tag !== OffscreenComponent) {
if (fiber.flags & PlacementDEV) {
if (isInStrictMode) {
runWithFiberInDEV(
fiber,
doubleInvokeEffectsOnFiber,
root,
fiber,
(fiber.mode & NoStrictPassiveEffectsMode) === NoMode,
);
}
} else {
recursivelyTraverseAndDoubleInvokeEffectsInDEV(
root,
fiber,
isInStrictMode,
);
}
return;
}
// ... and other code
} As we can see, these methods simply trigger the effects twice. If you look at the legacy code (the method on line 4286, with the name legacyCommitDoubleInvokeEffectsInDEV), then it's effects that are literally just called twice. Here's how these "double effects" are applied: function commitDoubleInvokeEffectsInDEV(
root: FiberRoot,
hasPassiveEffects: boolean,
) {
if (__DEV__) {
if (useModernStrictMode && (disableLegacyMode || root.tag !== LegacyRoot)) {
let doubleInvokeEffects = true;
if (
(disableLegacyMode || root.tag === ConcurrentRoot) &&
!(root.current.mode & (StrictLegacyMode | StrictEffectsMode))
) {
doubleInvokeEffects = false;
}
recursivelyTraverseAndDoubleInvokeEffectsInDEV(
root,
root.current,
doubleInvokeEffects,
);
} else {
// TODO: Is this runWithFiberInDEV needed since the other effect functions do it too?
runWithFiberInDEV(
root.current,
legacyCommitDoubleInvokeEffectsInDEV,
root.current,
hasPassiveEffects,
);
}
}
} Unfortunately, after reading the code, I cannot answer the question of why it is implemented this way, but I still assume that this is due to the stages of rendering in React: the first stage of processing (when, for example, the state has changed) it is not related to the echo stage of the useEffect call, and most likely StrictMode follows these steps sequentially, rather than just render the component twice. Also, after thinking about it a bit, I think this implementation is due to the fact that props from the parent component can be passed to the dependency array for useEffect, for example like this: export const ChildComponent = (props) => {
useEffect(() => {
// some logic here
}, [props])
return (
<div>Some Child Component</div>
)
} Of course, this is just a guess, but I assume that StrictMode can either avoid possible bugs of this kind (which are related to useEffect), or somehow prevent these errors, but I'm not sure, this test case came to my mind while I was writing this text 😶 Here's that file I was relying on. I took all the information from this file, because in the rest, except for processing warnings and working with devtools, I did not find anything, and I could have missed something, so if I find anything, I will write something new! |
After reading this thread , in general, my words are confirmed - StrictMode simply "simulates" a double render in its own way, rather than doing it for real, and, most likely, the stage with useEffect is placed in a separate stage of rendering precisely because the render is still like one, but there is another "simulated" re-render. There may be several reasons for this, but, as discussed there, it is related to various test cases that need to be processed. Of course, it described working with refs, but in general, I think this applies to useEffect too. I should have read this thread first to have at least some idea, but we have what we have :) As for the documentation, I'm not sure that the official React.dev documentation requires a full explanation of how the effects work with StrictMode, however, I think we can add a small paragraph stating that the effects behave a little differently than with manual re-rendering. |
@pamellix, Yeah that thread actually got me thinking about all of this. Since it is mentioned that React is really performing an unmount it was confusing why the order is different. Thank you for trying and for making an effort. |
Summary
This thread comes really close to avoid repeating this question again.
However I still have some doubts about the chain of the events in the strict mode.
I will quote the answer which provide the most valuable feedback.
"The mental model for this is that React really does unmount the component, but when re-mounting the component we restore the previously used state instead of starting fresh, the way Fast Refresh does, or some future features will."
But if that's the case why the order of the events(rendering, setting state, effects etc) is different while running in strict mode and outside of it?
Here are the concrete examples:
The second
render
andstate setter
are dimmed out according to the documentation and that's fine. So at least on the surface it appears that react is triggering render function (and state setter) 2 times before moving to the effects.This example has the mental model I have always worked with and which is clear, so I would waste too much time on it since that's how React also behaves on production.
The question is, if react is really performing mounting, unmounting and remounting while is the order different?
To be honest I haven't noticed that this difference is making my code harder to reason about while working with StrictMode.
I would like to know the details, if possible. What does this approach help React achieve that original one does not?
Page
https://react.dev/reference/react/StrictMode
Details
I think it would be helpful to update the page to explain why the behavior in StrictMode is different than simply running mounting, unmounting and mounting again.
The text was updated successfully, but these errors were encountered: