React 18 StrictMode first useEffect wrong state

1.1k Views Asked by At

another React 18 strict mode question. I'm aware React will call the render and effect functions twice to highlight potential memory leaks with the upcoming features. What I yet don't understand is how to properly handle that. My issue is that I can't properly unmount the first render result as the two useEffect calls are performed with the state of the 2nd render. Here is an example to showcase what I mean.


  const ref = useRef(9);
  const id = useId();

  console.log('@@ initial id', id);
  console.log('@@ initial ref', ref.current);

  ref.current = Math.random();

  console.log('@@ random ref', ref.current);

  useEffect(() => {
    console.log('@@ effect id', id);
    console.log('@@ effect ref', ref.current);

    return () => {
      console.log('@@ unmount id', id);
      console.log('@@ unmount ref', ref.current);
    };
  });

and here is the log output

@@ initial id :r0:
@@ initial ref 9
@@ random ref 0.26890444169781214
@@ initial id :r1:
@@ initial ref 9
@@ random ref 0.7330565878991766
@@ effect id :r1:                 <<--- first effect doesn't use data of first render cycle
@@ effect ref 0.7330565878991766
@@ unmount id :r1:
@@ unmount ref 0.7330565878991766
@@ effect id :r1:
@@ effect ref 0.7330565878991766

As you can see there is no useEffect call with the state of the first render cycle and as well the 2nd render cycle doesn't provide you with the ref of the first render cycle (it is initialized with 9 again and not 0.26890444169781214. Also the useId hook returns two different ids where the 2nd Id is kept also in further render cycles. Is this a bug or expected behavior? If it is expected, is there a way to fix this?

1

There are 1 best solutions below

6
On

Before React 18's StrictMode, your components would only mount once. But now, they mount, are unmounted, and then remounted. So its not only the effects that are ran twice - your entire component is rendered twice.

That means your state is re-initialized and your refs are also reinitialized. Obviously, your effects will run twice as well.

About effects running twice, you need to properly cleanup async effects - any effect that does something asynchronously, like fetching data from the server, adding an event listener etc. Not all effects need a cleanup.

Also, the effects are meant to run twice in development (they only run once in production). Some people try to prevent effects from running twice, but that is not okay. If you cleanup an effect properly, there should be no difference in its execution when it runs once in production or twice in development.

Also the useId hook returns two different ids where the 2nd Id is kept also in further render cycles. Is this a bug or expected behavior? If it is expected, is there a way to fix this? The second value will be the one that is used. It is not a bug, and you can keep using that as the "true" value.

You can readup more on StrictMode here.

Edit: Detecting an unmount.

// Create a ref to track unmount
const hasUnmounted = useRef(false)

useEffect(() => {
  return () => {
    // Set ref value to true in cleanup. This will run when the component is unmounted. If this is true, your component has unmounted OR the effect has run at least once
    hasUnmounted.current = true;
  }
}, [])