Skip to content
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

Open
Tosheen opened this issue Nov 29, 2024 · 9 comments
Open

[Suggestion]: StrictMode confusion about ordering of the events #7315

Tosheen opened this issue Nov 29, 2024 · 9 comments

Comments

@Tosheen
Copy link

Tosheen commented Nov 29, 2024

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:

  1. https://codesandbox.io/p/sandbox/2z4wq2. This sandbox runs inside a strict mode and by inspecting the console we can see that the order of the events is like this
render
state setter
render
state setter
effect
clean up
effect

The second render and state 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.

  1. https://codesandbox.io/p/sandbox/93zfkw. This sandbox runs outside of the strict mode and there is a button to toggle the mounting of the component. If the component is mounted, unmounted and mounted again the order of the events is like this.
render
state setter
effect
clean up
render
state setter
effect

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.

@pamellix
Copy link

pamellix commented Dec 7, 2024

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

image

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!

@branko-toshin-deel
Copy link

Hi, @pamellix.
I do get why having a double mount helps with detecting bugs earlier in the development stage and you examples are clear enough. However it still doesnt explain why the order is different.
Wouldnt the outcome be the same if effects ar run at the same stage they are run in non strict mode?
For example if I manually mount/unmount/mount component I would be able to catch the effect bugs like not clearing the timeouts etc...

@pamellix
Copy link

pamellix commented Dec 10, 2024

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

 "1 -> 2 -> 3 -> 1 -> 2 -> 3"

it tests them like this

"1 -> 1 -> 2 -> 2 -> 3 -> 3" 

(just my guess)

@branko-toshin-deel
Copy link

@pamellix
Yes thats the thing we can do now, to guess :). I would like to know the details behind that decision and what does it help achieve.

@pamellix
Copy link

@branko-toshin-deel
I think in the next few days I will look at the StrictMode source code and try to see in detail how it works, post comments of interesting points in this issue that I find and add to the documentation on React.dev

@branko-toshin-deel
Copy link

@pamellix That would be most appreciated.
Thanks for the effort.

@pamellix
Copy link

@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: /workspaces/react/packages/react-reconciler/src/ReactFiberWorkLoop.js, and here's what I found:

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!

@pamellix
Copy link

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.

@Tosheen
Copy link
Author

Tosheen commented Dec 27, 2024

@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.
Someone from the React team could probably clear the confusion very easily but for now I will just accept it as is :)

Thank you for trying and for making an effort.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants