A real React tree (development build, StrictMode on) mounting, unmounting, remounting and replacing DiagView diagrams. Any unexpected error would show up in a red box below; the deliberate "unsafe pattern" crash stays inside the viewer card.
In development, React StrictMode runs every effect twice: mount, cleanup, mount
again. For DiagView that means init() → destroy() → init()
in a row. The "effect runs" counter above shows 2 after load. Older builds bailed on the second
init and ended up with nothing initialized; now init waits for the pending destroy. Diagrams
initialise lazily as they scroll near the viewport, so "wrappers" reads 2 (floating + header)
once the viewer cards below are in view.
Unmount removes the floating and header viewer components; the lifecycle
cleanup calls destroy(), so "initialized" flips to NO on
purpose, the wrapper count drops to 0 and the off-layout diagram loses its click handler too.
Mount brings them back and DiagView re-initializes everything. Replace swaps Diagram A
for B by re-keying the container, never the diagram element itself. All three must run
with zero errors.
Unmounting is React deleting the component and every DOM node it rendered,
the diagram included. That is what unmount means in any framework, in every layout: the off-layout
card only survives here because it is never unmounted. DiagView does not delete anything; its
toolbar simply leaves together with the diagram it was attached to. There is nothing to fix.
If you want the diagrams to stay and only DiagView to go, do not unmount: call
destroy() instead. Detach DiagView does exactly that: the toolbars vanish,
the diagrams remain plain SVG, and Attach calls init() to enhance them
again.
Ticking it re-renders the floating and header viewers with the .diagram
element as the component root, with no container around it. Both layouts move that element into a
wrapper, so when you then click Unmount, React asks the parent it remembers to remove a node
that is no longer there. Expect a red
NotFoundError: The node to be removed is not a child of this node inside each of those
two cards. The off-layout card never re-parents, so it is unaffected. Each card has its own React
error boundary, so the crash stays in the card: click Mount viewers and DiagView
re-initialises (untick first for the safe pattern). Without a boundary React 18 would unmount the
entire root and only a page refresh would bring it back.
// SAFE — what React rendered // what the DOM looks like after DiagView init
<div class="diagram-host"> <div class="diagram-host"> ← React removes this
<div class="diagram">…</div> <div class="diagview-wrapper">
</div> <div class="diagview-viewport">
<div class="diagram">…</div> ← goes away with it
…
// UNSAFE — what React rendered // after DiagView init
<div class="diagram">…</div> <div class="diagview-wrapper">
<div class="diagview-viewport">
<div class="diagram">…</div> ← React still thinks this is
a child of the original parent
// On unmount React calls originalParent.removeChild(diagram) → NotFoundError
// Keep the diagram element inside a container the framework owns.
// floating/header layouts move .diagram into a toolbar wrapper; React must
// never remove that exact element itself — removing the container is safe.
function Viewer({ svg }) {
useEffect(() => {
DiagView.init({ layout: "floating" });
return () => DiagView.destroy(); // StrictMode: init() queues behind this
}, []);
return (
<div className="diagram-host"> // React owns this
<div className="diagram" dangerouslySetInnerHTML={{ __html: svg }} />
</div>
);
}
// layout "off" never touches the surrounding DOM, so .diagram may be the root.
<div className="diagram" data-diagview-layout="off">…</div>