React 18 in StrictMode

A real React tree (development build, StrictMode on) mounting, unmounting, remounting and replacing DiagView diagrams. Any unexpected error shows up in a red box below. The deliberate "unsafe pattern" crash stays inside its viewer card.

React 18.3 dev build StrictMode double effects Floating, header and off layouts
Effect runs (init calls)0
DiagView initialized...
Toolbar wrappers in DOM0
Unexpected errors0


            

Live diagrams

What this page tests

Four things a component framework does to a diagram, and what DiagView does in each case.

StrictMode

What StrictMode does here

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 and header) once the viewer cards are in view.

Lifecycle

Unmount, mount and replace

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.

Detach

Why unmount removes the diagram itself

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 leaves together with the diagram it was attached to. 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.

Anti-pattern

The "unsafe pattern" checkbox

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 inside each of those two cards. The message after it depends on the browser. 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.

Why the container matters

Floating and header layouts move the diagram into a wrapper. React must only ever remove an element it still owns.

What React renderedSafe
<div class="diagram-host">
  <div class="diagram">...</div>
</div>
After DiagView initSafe
<div class="diagram-host">
  <div class="diagview-wrapper">
    <div class="diagview-viewport">
      <div class="diagram">...</div>
  ...
// React removes .diagram-host,
// and .diagram goes away with it
What React renderedUnsafe
<div class="diagram">...</div>
After DiagView initUnsafe
<div class="diagview-wrapper">
  <div class="diagview-viewport">
    <div class="diagram">...</div>
// React still thinks .diagram is
// a child of the original parent
On unmountUnsafe
// React calls
originalParent.removeChild(diagram)
// and gets a NotFoundError

The one rule

Viewer.jsx
// Keep .diagram inside a container
// the framework owns. Floating and
// header layouts move .diagram into
// a toolbar wrapper, so React must
// never remove that element itself.
// Removing the container is safe.
function Viewer({ svg }) {
  useEffect(() => {
    DiagView.init({
      layout: "floating"
    });
    // StrictMode: init() waits
    // for this destroy()
    return () => DiagView.destroy();
  }, []);

  return (
    // React owns this container
    <div className="diagram-host">
      <div
        className="diagram"
        dangerouslySetInnerHTML={{
          __html: svg
        }}
      />
    </div>
  );
}

// Layout "off" never touches the
// surrounding DOM, so .diagram may
// be the component root.
<div
  className="diagram"
  data-diagview-layout="off"
>...</div>