React 18 async way to unmount root
Asked Answered
C

1

13

I have a rare usecase where I need to register multiple roots in my React Component and destroy them when the component unmounts. Obviously, it happens that I unmount a root when its rendering. I simulated that case by calling root.unmount() right after root.render(...). in the following example: https://codesandbox.io/s/eager-grothendieck-h49eoo?file=%2Fsrc%2FApp.tsx

This results in the following warning: Warning: Attempted to synchronously unmount a root while React was already rendering. React cannot finish unmounting the root until the current render has completed, which may lead to a race condition.

This warning implies to me that there is an async way to unmount a root, but I coouldn't find out how. Wrapping root.unmount() in an async function (const unmount = async () => root.unmount()) did not work. Any ideas? Am I getting something totally wrong here?

Christmastide answered 23/8, 2022 at 13:13 Comment(4)
Does this help ? stackoverflow.com/a/72198112Theodora
Sadly not, @Dilshan. This does unmount the element, but doesn’t guarantee it happens while React isn’t mid-render.Candlenut
I have the same issue, how did you resolve yours ? #74389558Rothschild
I didn't. For now I just live with it, but it'd be great if there was a solution.Christmastide
D
11

In our project we're unmounting asynchronously via setTimeout. I created an updated codesandbox, you can find the working example here.

The snippet below shows how the mounting and unmounting can be handled. Note that mounting and unmounting are detached from the synchronous rendering loop via setTimeout, this is to avoid the racing between synchronous mount and asynchronous unmount. Otherwise it can happen that the component is unmounted right after it has been mounted from a previous render.

I'm not convinced this is the best solution, but it's working for us so far.

function MyComponent() {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const rootRef = useRef<ReactDOM.Root>();

  useEffect(() => {
    const renderTimeout = setTimeout(() => {
      if (containerRef.current) {
        console.log("create root");
        rootRef.current =
          rootRef.current ?? ReactDOM.createRoot(containerRef.current);
      }

      if (containerRef.current && rootRef.current) {
        console.log("component render");
        rootRef.current.render(<div>mounted component</div>);
      }
    });

    return () => {
      clearTimeout(renderTimeout);
      console.log("unmount");
      const root = rootRef.current;
      rootRef.current = undefined;

      setTimeout(() => {
        console.log("component unmount");
        root?.unmount();
      });
    };
  }, [rootRef]);

  return <div ref={containerRef}></div>;
}
Doronicum answered 15/11, 2022 at 12:37 Comment(9)
Thanks for the answer. I've tried to integrate this into my codebase, but I had no success. I have some more quirks in my actual usecase that might be the reason for this. If anyone else confirms that this worked with their usecase I will accept this as the correct answer.Christmastide
@Christmastide Actually, in our project it's not that simple either, because there are some more props in play. The solution there was to put the unmount in a separate useEffect, see this codesandbox. The reason being, that if you have more dependencies in the useEffect it will attempt to unmount the component when they change. Hope this helps.Doronicum
I am not a fun of this solution, timeouts are tricky, but it is working 👍. I think this react warning can be ignored, as it does the unmount anyway, and in prod build there is no warning.Rothschild
I don't like the timeouts either. But there are two reasons why I believe it's okay here. 1.) The timeouts are not used as real timeouts with delay, but as a way to defer execution until the react in the host is done 2.) React is telling us to do exactly that in the log message. Thing is, we're trying to mount a react app in another running react app, so according to the log message we need to break out of the render loop and do the inner render "outside" of the outer react. It's working but maybe not the best solution. If I come across a better one, I'll update the answer.Doronicum
Potential related issue: github.com/facebook/react/issues/25675Joist
this is a very informative answer, thank you @konqui. do you know how this could be adapted to use hydrateRoot instead of createRoot+render?Curet
@ColinD Sounds like a different kind of issue to me. However, if you create a codesandbox to demonstrate the problem I can have a look.Doronicum
@Doronicum I tried adopting your example in this codesandbox here codesandbox.io/s/cocky-pond-vpcmr7?file=/src/App.tsx it currently gives an error "React instrumentation encountered an error: Error: Expected root pseudo key to be known." it tries to repeatedly show and hide the SSR content because my app has this happen rapidly during scroll so it sometimes uncovers weird errorsCuret
From what I understand that error is an issue between react and react dev tools. It shouldn't affect your production application.Doronicum

© 2022 - 2024 — McMap. All rights reserved.