Skip to content

Bug: False positive warning with nested roots: Attempted to synchronously unmount a root while React was already rendering. #25675

@aovchinn

Description

@aovchinn

we get a warning on (legitimate?) use case, if there is a manually added react root inside another parent root
(for example for part of Backbone view that is inserted into parent react component)
on unmount we can see that child root is in commit context, so we cant unmount it

React version: 18.2

Steps To Reproduce

  1. have nested react roots, child is manually added/removed on parent mount/unmount
  2. unmount parent root

I have 2 way bindings for backbone.marionette and react

  • reactToMarionette
  • useMarionetteInReact hook
    when root/parent component gets rendered/destroyed React gives this 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.

I am looking for ways to fix this warning

I think ReactChild node is somehow marked as toBeRendered at the app render, even though I would expect that app.root would not know about nested/inserted MView react root

Link to code example: https://codesandbox.io/s/my-test-adapters-forked-tdbgdb

problematic code
import { View } from "backbone.marionette";
import React, { useCallback, useRef, useState } from "react";

import { createRoot } from "react-dom/client";

export function App() {
  // create a reason to render inner component
  const [isVisible, setVisible] = useState(true);
  const toggle = useCallback(() => setVisible((i) => !i), []);

  console.log("render app", isVisible);
  return (
    <>
      <button onClick={toggle}>{`toggle: ${isVisible}`}</button>
      {isVisible ? <SomeComponent /> : null}
    </>
  );
}

// component that children are controlled from outside (by marionette)
const SomeComponent = () => {
  console.log("render SomeComponent");
  const ref = useMarionetteInReact();
  return <div ref={ref} className="stable-react-div"></div>;
};

// hook, for rendering marionette view
const useMarionetteInReact = () => {
  const viewRef = useRef(null);

  const divRef = useCallback((el) => {
    if (el === null) {
      console.log("MView destroy in useCallback", viewRef.current);
      viewRef.current && viewRef.current.destroy();
    } else {
      console.log("created MView");
      const MView = new reactToMarionette({
        className: "reactToMarionette",
        template: false,
        component: <ReactChild />
      });
      viewRef.current = MView;
      MView.render();
      el.appendChild(MView.el);
    }
  }, []);

  return divRef;
};

class reactToMarionette extends View {
  constructor(options) {
    super(options);
    this.component = options.component;
    this.el.textContent = "I am Marionette";

    console.log("create root", this.el);
    this.divEl = document.createElement("div");
    this.divEl.classList.add("portal-root");
    this.root = createRoot(this.divEl);
    this.el.append(this.divEl);
  }

  render() {
    console.log("MView render");
    this.root.render(this.component);
  }

  onBeforeDestroy() {
    console.log("onBeforeDestroy", this.root);
    if (this.root) {
      // setTimeout(() => this.root.unmount());
      this.root.unmount();
    }
  }
}

const ReactChild = () => {
  console.log("render ReactChild");
  return <div> Hello, I am react child </div>;
};

The current behavior

warning is displayed

The expected behavior

no warning ?

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions