honojs/hono · error · Error

Cannot update an unmounted root

Error message

Cannot update an unmounted root

What it means

This error comes from hono/jsx/dom/client's createRoot/hydrateRoot handle. Calling render() on a root whose internal setJsxNode is null (i.e. the root was unmounted or never fully initialized) throws, because there is no live rendering pipeline to update.

Source

Thrown at src/jsx/dom/client.ts:41

export const createRoot = (
  element: HTMLElement | DocumentFragment,
  options: RootOptions = {}
): Root => {
  let setJsxNode:
    | undefined // initial state
    | ((jsxNode: unknown) => void) // rendered
    | null = // unmounted
    undefined

  if (Object.keys(options).length > 0) {
    console.warn('createRoot options are not supported yet')
  }

  return {
    render(jsxNode: unknown) {
      if (setJsxNode === null) {
        // unmounted
        throw new Error('Cannot update an unmounted root')
      }
      if (setJsxNode) {
        // rendered
        setJsxNode(jsxNode)
      } else {
        renderNode(
          buildNode({
            tag: () => {
              const [_jsxNode, _setJsxNode] = useState(jsxNode)
              setJsxNode = _setJsxNode
              return _jsxNode
            },
            props: {},
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
          } as any) as NodeObject,
          element
        )
      }

View on GitHub (pinned to e2740d5a1b)

Solutions

  1. Stop calling render() after unmounting; null out references to the root when you unmount it
  2. Re-create the root with createRoot(container) before rendering again
  3. In component lifecycles, ensure any scheduled render is cancelled in cleanup (guard with a mounted flag)

Example fix

// before
const root = createRoot(container)
unmountFn = () => container.remove()
// later, after container removed:
root.render(<App />) // throws
// after
const root = createRoot(container)
unmountFn = () => {
  root.unmount()
  rootRef.current = null
}
if (rootRef.current) rootRef.current.render(<App />)
Defensive patterns

Strategy: validation

Validate before calling

let root: Root | null = createRoot(container)
const safeRender = (node: unknown) => { if (root) root.render(node) }

Type guard

const isLiveRoot = (r: { render: unknown } | null): boolean => r !== null

Try / catch

try {
  root.render(<App />)
} catch (e) {
  if (e instanceof Error && /unmounted root/.test(e.message)) {
    root = createRoot(container)
    root.render(<App />)
  }
}

Prevention

When it happens

Trigger: Calling root.render(...) after the root was unmounted, or rendering to a root created for an element that was removed from the DOM; holding a stale root handle from a previous page/component lifecycle and reusing it after remount.

Common situations: SPAs or islands where cleanup unmounts roots on route change but cached component instances still call render; effects that run after unmount (missing cleanup in useEffect-equivalent) triggering a state-driven render; HMR or strict-mode double-mounting dropping references.

Related errors


AI-assisted analysis of honojs/hono@e2740d5a1b (2026-08-28). Data as JSON: /api/errors/af55f2f3572cf4c3. Report an issue: GitHub.