pmndrs/react-three-fiber · warning

RTTR: attempted to update an unmounted root!

Error message

RTTR: attempted to update an unmounted root!

What it means

The test-renderer's root.update() checks the internal mockRoots registry; if the canvas key is absent the root was already unmounted, so it warns 'RTTR: attempted to update an unmounted root!' and skips the render instead of acting on a dead root.

Source

Thrown at packages/test-renderer/src/index.tsx:61

    scene: wrapFiber(_scene),
    async unmount() {
      await act(async () => {
        _root.unmount()
      })
    },
    getInstance() {
      // Bail if canvas is unmounted
      if (!mockRoots.has(canvas)) return null

      // Traverse fiber nodes for R3F root
      const root = { current: mockRoots.get(canvas)!.fiber.current }
      while (!root.current.child?.stateNode) root.current = root.current.child

      // Return R3F instance from root
      return reconciler.getPublicRootInstance(root)
    },
    async update(newElement: React.ReactNode) {
      if (!mockRoots.has(canvas)) return console.warn('RTTR: attempted to update an unmounted root!')

      await act(async () => {
        _root.render(newElement)
      })
    },
    toTree() {
      return toTree(_scene)
    },
    toGraph() {
      return toGraph(_scene)
    },
    fireEvent: createEventFirer(act, _store),
    async advanceFrames(frames: number, delta: number | number[] = 1) {
      const state = _store.getState()
      const storeSubscribers = state.internal.subscribers

      const promises: Promise<void>[] = []

View on GitHub (pinned to ff3899dbf4)

Solutions

  1. Check the root's mounted state (or recreate via createRoot) before calling update/advance
  2. Move advance()/update calls before unmount/afterEach teardown
  3. Ensure beforeEach/afterEach correctly create and dispose rttr roots so cached references aren't reused
  4. Await all pending act() promises before unmounting so no tick fires afterwards

Example fix

// before
const root = createRTTRRoot(canvas)
root.render(<Scene />)
root.unmount()
root.update(<Scene v2 />) // warns

// after
const root = createRTTRRoot(canvas)
root.render(<Scene />)
root.update(<Scene v2 />)
root.unmount()
Defensive patterns

Strategy: type-guard

Validate before calling

let disposed = false
const safeUpdate = (el: React.ReactNode) => {
  if (disposed) return
  root.update(el)
}

Type guard

const isMounted = (root: { unmount: () => void } & Record<string, any>): boolean =>
  mockRoots.has(root.canvas) // or track a local `disposed` flag set in unmount wrapper

Prevention

When it happens

Trigger: Keeping a reference to an rttr root after calling root.unmount() (or after the test finished/tore down) and then calling root.update(element) or advance()/loop() ticks that render.

Common situations: Test helpers that cache roots across tests without resetting, awaiting act() after unmount, or calling advance() in a fake-timer loop after the root was cleaned up in afterEach.

Related errors


AI-assisted analysis of pmndrs/react-three-fiber@ff3899dbf4 (2026-08-28). Data as JSON: /api/errors/963ce4c83b317a6e. Report an issue: GitHub.