{"record":{"id":"963ce4c83b317a6e","repo":"pmndrs/react-three-fiber","slug":"rttr-attempted-to-update-an-unmounted-root","errorCode":null,"errorMessage":"RTTR: attempted to update an unmounted root!","messagePattern":"RTTR: attempted to update an unmounted root!","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/test-renderer/src/index.tsx","lineNumber":61,"sourceCode":"    scene: wrapFiber(_scene),\n    async unmount() {\n      await act(async () => {\n        _root.unmount()\n      })\n    },\n    getInstance() {\n      // Bail if canvas is unmounted\n      if (!mockRoots.has(canvas)) return null\n\n      // Traverse fiber nodes for R3F root\n      const root = { current: mockRoots.get(canvas)!.fiber.current }\n      while (!root.current.child?.stateNode) root.current = root.current.child\n\n      // Return R3F instance from root\n      return reconciler.getPublicRootInstance(root)\n    },\n    async update(newElement: React.ReactNode) {\n      if (!mockRoots.has(canvas)) return console.warn('RTTR: attempted to update an unmounted root!')\n\n      await act(async () => {\n        _root.render(newElement)\n      })\n    },\n    toTree() {\n      return toTree(_scene)\n    },\n    toGraph() {\n      return toGraph(_scene)\n    },\n    fireEvent: createEventFirer(act, _store),\n    async advanceFrames(frames: number, delta: number | number[] = 1) {\n      const state = _store.getState()\n      const storeSubscribers = state.internal.subscribers\n\n      const promises: Promise<void>[] = []\n","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/pmndrs/react-three-fiber/blob/ff3899dbf43d2a88895fecf53c147192abfd7431/packages/test-renderer/src/index.tsx#L43-L79","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check the root's mounted state (or recreate via createRoot) before calling update/advance","Move advance()/update calls before unmount/afterEach teardown","Ensure beforeEach/afterEach correctly create and dispose rttr roots so cached references aren't reused","Await all pending act() promises before unmounting so no tick fires afterwards"],"exampleFix":"// before\nconst root = createRTTRRoot(canvas)\nroot.render(<Scene />)\nroot.unmount()\nroot.update(<Scene v2 />) // warns\n\n// after\nconst root = createRTTRRoot(canvas)\nroot.render(<Scene />)\nroot.update(<Scene v2 />)\nroot.unmount()","handlingStrategy":"type-guard","validationCode":"let disposed = false\nconst safeUpdate = (el: React.ReactNode) => {\n  if (disposed) return\n  root.update(el)\n}","typeGuard":"const isMounted = (root: { unmount: () => void } & Record<string, any>): boolean =>\n  mockRoots.has(root.canvas) // or track a local `disposed` flag set in unmount wrapper","tryCatchPattern":null,"preventionTips":["Wrap unmount to set a disposed flag and check it before update/advance","Create fresh roots per test in beforeEach; dispose in afterEach","Await all act() promises before teardown so no late tick renders a dead root"],"tags":["test-renderer","unmounted-root","lifecycle","update-after-unmount"],"backgroundTag":"update-after-unmount","analyzedSha":"ff3899dbf43d2a88895fecf53c147192abfd7431","analyzedAt":"2026-08-28T10:16:38.568Z","schemaVersion":2},"datasetVersion":"2026-08-28T11:17:15.048Z"}