A React component hands a `<div>` to an editor library that fills it with its own DOM. Why must React never render children inside that div, and what breaks if it does?
answer
- React diffs against its own model
- library nodes are invisible to that model
- container must be a leaf
- portal into a node the library made
basics
~20 sReact owns the children of the elements it renders and diffs them against its own model. Library-inserted nodes are invisible to that model, so a React update can remove or reorder the wrong nodes, or throw when a child it expected is gone.
solid answer
~50 sReact keeps an internal model of the children it created for a node and updates the real DOM by comparing against that model — it inserts before a sibling it believes is there, and removes nodes it believes it owns. A library that writes its own markup into the same parent is invisible to that bookkeeping. So when a conditional child flips or a list reorders, React can insert content in the middle of the library's markup, remove a node the library already replaced, or call `removeChild` on something that is no longer a child, which surfaces as the DOM exception about a node not being a child of this node. The library, meanwhile, sees its markup mutated underneath it and misbehaves. The rule is that the container is a leaf in React's tree: render `<div ref={ref} />` with nothing inside. React content goes as a sibling, or through `createPortal` into a node the library itself created and handed you.
code
javascript · 22 linesimport { useEffect, useRef } from 'react';
import { EditorView } from '@codemirror/view';
export function Editor() {
const containerRef = useRef(null);
useEffect(() => {
// React owns the outer div; this host node is created and
// removed imperatively, so React never diffs it.
const host = document.createElement('div');
containerRef.current.appendChild(host);
const view = new EditorView({ parent: host });
return () => {
view.destroy();
host.remove();
};
}, []);
return <div ref={containerRef} />;
}go deeper
Remember the rule and apply it: the element you hand to a library carries a ref and nothing else. Put any React UI next to it, never inside it.
Explain the mechanism — React diffs against its own record of the children it created, so nodes it never made are invisible and its insert and remove instructions target the wrong things.
Recognise the symptom in the wild: an unrelated state change throwing a removeChild DOMException, or an editor losing its selection after a sibling re-renders, and know that createPortal into a library-created node is the sanctioned escape hatch.
Treat DOM ownership as an interface contract in the codebase: which subtree belongs to which system must be explicit in the wrapper, so integrations do not degrade into mutation races that only reproduce under specific update orders.
## What React assumes about the nodes it renders React does not read the DOM to decide what to change. It keeps its own tree of fibers describing what it rendered last time, compares it to what you returned this time, and emits a minimal list of DOM mutations: create this node, insert it before that one, remove this one, set that attribute. Every one of those instructions is computed from React's model, never from the live document. That is fast, and it is correct exactly as long as one thing holds: the children of a node React created are the children React put there. A third-party library that appends, replaces or reorders markup inside the same parent violates the assumption silently. React does not crash at that moment; it just now holds a model of the parent's children that no longer matches reality. ## What actually breaks **Insertions land in the wrong place.** React inserts a new child "before" a reference node it remembers. If the library has inserted its own nodes ahead of that reference, your new element appears in the middle of the library's markup rather than where the JSX says it should. **Removals hit the wrong node, or nothing at all.** When a conditional child disappears, React calls `removeChild` with the node it created. If the library already replaced or moved that node, the call fails with a `DOMException` saying the node to be removed is not a child of this node. This is the single most common way the bug is reported, and it usually appears long after the offending code, during an unrelated state change. **The library gets confused too.** Editors and charts cache references to their own nodes and register listeners on them. If React removes one of those nodes because it thinks it owns that slot, the library keeps a reference to a detached element: selections stop working, redraws paint into nothing, and event handlers fire against a subtree no longer in the document. **Hydration turns the mismatch structural.** Server-rendered markup for a container that React believes has children must line up with what the client renders; a library that mutates the container before or during hydration makes that comparison meaningless. ## The rule The container is a **leaf** in React's tree: ```js return <div ref={containerRef} />; // nothing inside, ever ``` No conditional spinner, no `{children}`, no `dangerouslySetInnerHTML`, no text node. If React must not render into it, React and the library can never disagree about it. ## Where the React UI goes instead **As siblings.** A toolbar, an empty state, a loading indicator or an error message renders next to the container, inside a wrapper React fully owns. This covers most cases. **Through a portal.** When React content genuinely must appear *inside* library-owned markup — the body of a map popup, a custom cell in a grid — ask the library for the DOM node it created for that slot and render into it with `createPortal`. The node's parent is owned by the library; the node's contents are owned by React; each side has an undisputed subtree. **Through the library's own API.** Many libraries accept a string or a render callback for their slots. Producing that content with React and handing over a node is usually cleaner than fighting the library's templating. ## The defensive variant Even with an empty container you are trusting the library's destructor to leave the div clean. A stronger version has the effect create its own host node: ```js useEffect(() => { const host = document.createElement('div'); containerRef.current.appendChild(host); const view = new EditorView({ parent: host }); return () => { view.destroy(); host.remove(); }; }, []); ``` Now React owns an outer div whose only child was created and removed by imperative code React never diffs, and teardown is guaranteed to leave the container genuinely empty. This also rescues libraries that refuse to initialize twice against the same element, because every setup gets a brand-new host. ## How to spot the mistake in review Any JSX where a `ref` handed to a library sits on an element with children, a conditional, a mapped list or a `{children}` slot is the defect — regardless of whether it currently works. It works until the first update that touches that subtree, which is why it typically ships and then breaks in a later, unrelated feature.
- The error is a DOMException about a node not being a child of this node. How do you trace it back to the cause?Read it as "React tried to remove a node it believed it owned". Find which component's update triggered the commit from the stack, then look for a ref handed to a third-party library on an element that also has React children — a conditional or a mapped list is the usual culprit. The fix is to empty the container, not to guard the removal.
- Is `dangerouslySetInnerHTML` on the container an acceptable way to seed markup the library then takes over?No. React treats that HTML as content it owns and will reset it whenever the prop changes, wiping whatever the library built. If the library needs starting markup, pass it through the library's own options, or build the node imperatively in the effect so React never diffs it.
- How does this interact with server rendering the wrapper?The server renders the empty container and nothing else, and the effect that initializes the library never runs there — so the client's first render matches the server's markup exactly. That clean match is another reason to keep the container empty; a container with React children plus library mutations makes the initial comparison unpredictable.
saying these in an interview costs you the question
- Puts a conditional spinner inside the library's container
- Thinks React re-creates library nodes from the virtual DOM
- Uses dangerouslySetInnerHTML on the container
- Treats the removeChild exception as a React bug
- Renders {children} into a node the library manages