reactjs / react.dev

The React documentation website
https://react.dev/
Creative Commons Attribution 4.0 International
10.9k stars 7.45k forks source link

React 18 strict mode docs are inconsistent about how unmount and remount actually works (and whether it's simulated) #6123

Closed AleksandrHovhannisyan closed 11 months ago

AleksandrHovhannisyan commented 1 year ago

Problem

The React v18.0 release article states the following about React 18's new strict mode behavior. Emphasis is my own:

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.

However, in the following code demo, I'm able to use a ref to keep track of the number of times that useEffect runs, which I would not be able to do if the component were truly unmounting since refs are destroyed: https://codesandbox.io/s/run-mount-logic-only-once-zpywhc?file=/src/App.js.

import { useEffect, useRef } from "react";

export default function App() {
  const hasMounted = useRef(false);

  useEffect(() => {
    if (hasMounted.current === false) {
      hasMounted.current = true;
      console.count("mounted");
    }
  }, []);

  return <></>;
}

If the component truly unmounted and remounted and then restored state on the second mount, then the ref would not persist because 1) refs are not state (and hence not restored as described in the above quote), and 2) refs are normally destroyed on unmount. In that case, we would see the following logs:

mounted: 1
mounted: 2

However, what we actually see is:

mounted: 1

Which seems to suggest that React is not actually unmounting and remounting every component.

But later on, the same article clarifies that React simulates unmounting and remounting:

With Strict Mode in React 18, React will simulate unmounting and remounting the component in development mode:

Which makes a lot more sense: Effects are invoked twice to simulate an unmount and remount, but components are not truly unmounted. The React docs on [Fixing bugs found by re-running Effects in development]() further clarify this:

When Strict Mode is on, React will also run one extra setup+cleanup cycle in development for every Effect. This may feel surprising, but it helps reveal subtle bugs that are hard to catch manually.

Suggestion:

There are two possibilities as far as I can tell:

  1. React does actually unmount and remount components, and it restores not only "state" but also anything within the scope of the component (e.g., refs).
  2. (More likely) React simulates remounting by simply invoking every effect twice.

Either way, I think it would be helpful to update all docs to be consistent. There's already quite a lot of misinformation out there on this subject and I've come across so many articles that claim React unmounts and remounts in strict mode on v18. For example, AG Grid's React 18 - Avoiding Use Effect Getting Called Twice article starts with the following:

React 18 introduced a huge breaking change, when in Strict Mode, all components mount and unmount, then mount again.

rickhanlonii commented 11 months ago

Yeah I can understand the confusion. Mount and unmount is not rigorously defined, so the difference between "actual" mount/unmount and "simulated" mount/unmount is unclear. For example, if state isn't cleaned up when doing the unmount, does that mean it's actually unmounted either? 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.

However, for refs specifically, we intend to update Strict Mode to also destroy and re-create refs during the simulated unmount/remount, since this is the behavior that will happen in production features: https://github.com/facebook/react/pull/25049. We're still rolling this change out and I don't have a timeline for when it will land, but the idea is that we're simulating an actual unmount/remount.

AleksandrHovhannisyan commented 11 months ago

@rickhanlonii In the meantime, would it be possible to keep this issue open as a reminder to update the website docs to be consistent?

Also, does this mean that devs shouldn't use the isMounted ref pattern since that will eventually break once React destroys and recreates refs in a future version?

rickhanlonii commented 11 months ago

Sorry, which part need updated?

AleksandrHovhannisyan commented 11 months ago

I noted this in my original post, but:

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.

vs.

With Strict Mode in React 18, React will simulate unmounting and remounting the component in development mode:

This difference in wording can cause some confusion, and it's an important distinction—because at the moment, a true unmount will destroy refs, while a simulated unmount does not. Everyone I asked (both at work and online) couldn't give me an answer because they were equally confused.

rickhanlonii commented 11 months ago

I don't understand how the word "simulated" matters. In strict mode, all the behaviors are obviously simulated because they're not real behaviors. But even if there was a difference, there should be not difference between a "simulated" unmount and a "real" unmount (the ref bug notwithstanding).

So it's not clear what the suggestion here is. If it's to add "simulated", sure I guess, but that doesn't change the meaning because "simulated" doesn't imply "doesn't include refs/state". If it's to remove "simulated" from the blog post, what would you call what Strict Mode does?

jorjordandan commented 11 months ago

Is the ref persisting actually a bug? If it's a bug that's going to be fixed it doesn't matter, but if refs are handled differently between dev and prod, even if it's a minor difference, that's important to know. It could lead to some confusing behaviour. Also it could be that using a ref like that is considered an undesirable side effect, and something to avoid, which would also be good to know.

AleksandrHovhannisyan commented 11 months ago

(the ref bug notwithstanding).

I have the same question as @jorjordandan—does this mean that "refs not getting destroyed" is actually a bug? If so, maybe it would be worth discouraging the "isMounted" ref pattern prominently in the docs somewhere. I've seen some tutorials suggesting that as a workaround (as opposed to disabling strict mode), and it's something we adopted in our code base so we could upgrade to React 18. But it sounds like that's a hacky workaround that will eventually break. Either way, it seems important enough to clarify in the docs.

So it's not clear what the suggestion here is. If it's to add "simulated", sure I guess, but that doesn't change the meaning because "simulated" doesn't imply "doesn't include refs/state". If it's to remove "simulated" from the blog post, what would you call what Strict Mode does?

The only reason I found the first description (the one without "simulated") confusing is because of the ref behavior—my (mis)understanding was that refs are preserved between these unmounts and remounts intentionally, in contrast to how ordinary remounts work in all other scenarios (state and refs are destroyed). So I took the word "simulated" to mean not only "this doesn't happen in prod" but also "it doesn't behave the same as ordinary remounts." But again, it sounds like that wasn't the intention?

rickhanlonii commented 11 months ago

StrictMode should simulate the prod behavior so it's a known gap we intend to fix.

maybe it would be worth discouraging the "isMounted" ref pattern prominently in the docs somewhere

Yeah, maybe. We have docs for handling the common cases and using a ref is not one of the ways to handle it.

It's important to understand what is actually happening in prod when you use a ref isMounted pattern, because it's probably not what you want anyway (even if we were to keep the current ref behavior).

In features like Fast Refresh and eventually Offscreen, we will unmount and remount the component, restoring the state that was last used. This is a benefit of those features, not just something React is imposing on you unnecessarily. In Fast Refresh, you don't want state like controlled input fields or filter settings to reset every time you change some CSS. So we unmount and remount the component, using the same state as before. In Offscreen, if you open a modal and close it, you probably don't want all the things behind the modal to reset.

With the features in mind, consider some common cases of using effects, and how the isMounted ref hack breaks them:

So even if we were going to persist refs, you wouldn't want to use the isMounted pattern. Hopefully the explanation above helps understand how this is a necessary constraint to get the benefits of the features, not something being imposed arbitrarily.

AleksandrHovhannisyan commented 4 months ago

Today I remembered this popular Reactathon 2022 talk by @davidkpiano that mentioned the ref workaround for strict mode. This may or may not have been the first time we saw the ref trick mentioned, but I remember it got a lot of attention on Twitter at the time and there were various discussions about this behavior and the proposed escape hatch.

Execute on mount, only once, from a talk by David Khourshid. The code uses a useRef to ignore the second mount in local dev.

Just revisiting this issue because I think we're underestimating just how widespread this pattern (anti-pattern?) may be at this point. Even though the docs themselves don't mention this pattern, other sites/resources do, and I think it's worth educating developers that this pattern may fail in the future.

rickhanlonii commented 2 months ago

Docs added here: https://github.com/reactjs/react.dev/pull/6846