@justuswitmer/fiber-inspector
v0.1.1
Published
In-app overlay for exploring a running React app's Fiber tree.
Maintainers
Readme
React Fiber Inspector
An in-app overlay for exploring a running React app. Hover any rendered element to see the component behind it, its props, hook/class state, subscribed context, ancestors, and the tree of what it renders. Click to pin and walk the tree. The goal is to learn an unfamiliar codebase by exploring its live UI instead of reading files.
It reads React's internal Fiber tree directly off the DOM at runtime. No browser extension, no DevTools hook, no required build step. Works on React 17, 18, and 19.
Features
Panel — props, named hooks (
useState (0)instead ofhook 0, from dev-build_debugHookTypes), subscribed context, ancestors breadcrumb, rendered-structure tree. Sections collapse individually; click a section title.Component map (⌗ map) — graph centered on the selected component: ancestor chain branching left, descendant tree branching right. Click any node to re-center. Export as Mermaid or JSON from the header.
App map (⌗ in the toolbar) — the same graph for the entire mounted tree, from the app root down, with zoom controls and Mermaid/JSON export. Click any node to select it in the panel. "Your components" mode keeps it readable; "all" mode shows everything.
All-source mode in the app map — the fiber tree only contains mounted components, so route-gated components (cart, checkout, etc.) are invisible until you navigate to them. The bundled scanner (
scan-components.cjs) statically analyzes yoursrc/and produces a JSON graph of every component and what it renders. Pass it via<Inspector staticMap={map} />and the app map gains a "mounted / all source" toggle: green = mounted now, gray = defined but not mounted; clicking a mounted node inspects it, clicking an unmounted one copies its file path. Regenerate on dev start with aprestartscript:"prestart": "node node_modules/@justuswitmer/fiber-inspector/scan-components.cjs src --out src/fiberInspectorMap.json"import staticMap from "./fiberInspectorMap.json"; <Inspector staticMap={staticMap} />;Source (file:line) — copy the full path or open it in your editor (
editorUrlTemplateprop; defaults to VS Code). See "Source locations" below.Search — type a name in the panel to find every mounted component matching it; click to select and scroll to it.
Instance cycling — when several instances of the selected component are mounted, ‹ › steps through them.
Keyboard walking — while pinned: ↑ parent, ↓ first child, ←/→ siblings.
Render tracking (⚡) — flashes components on screen as they re-render, counts renders of the selection, and lists which props changed in its last re-render. Detection works by polling for fiber-pair swaps; no DevTools hook.
Live watch — while pinned, the panel re-reads the fiber every 400ms so values stay current.
Mount snapshot/diff (◎ / Δ) — snapshot what is mounted, interact with the app, then diff to see which components mounted/unmounted and count changes.
State editing —
useStatehooks and class state with primitive values get a ✎ button; commit a new value and watch the UI respond. Edits dispatch through React, so they are real state updates.Owner row — "rendered by" shows the component whose JSX created the selection when it differs from the nearest ancestor (dev builds).
Source locations
The source section shows file:line for the selected component, with a "copy path" button (copies the full absolute path:line:column for pasting into an IDE) and an "open" link that launches your editor. The link defaults to VS Code; override it via the editorUrlTemplate prop, e.g. <Inspector editorUrlTemplate="cursor://file/{path}:{line}:{column}" />.
Where the location comes from: on React 17/18 dev builds it is read from the fiber's _debugSource. React 19 removed that field, so the Babel plugin now also records Component.__sourceLoc = "file:line:column" at build time, which the inspector reads as a fallback. Add the plugin (see its header comment for Vite/Babel setup) to get locations on React 19 — strict mode is not required.
Installation
This package is published as @justuswitmer/fiber-inspector on npm.
Install:
npm install @justuswitmer/fiber-inspectorThen import by name:
import { Inspector } from "@justuswitmer/fiber-inspector";
function App() {
return (
<>
<YourApp />
{process.env.NODE_ENV !== "production" && <Inspector />}
</>
);
}react is a peer dependency (v17+) and is not bundled; it resolves from the consuming app. Render <Inspector /> once, near the app root, behind a dev check.
The optional Babel plugin is exposed at a subpath:
const tagAppComponents = require("@justuswitmer/fiber-inspector/babel-plugin");Configuring which components are "yours"
React records no source file on a fiber, so there is no runtime way to tell app code from node_modules code. The inspector offers two strategies, configured by props rather than by editing source.
Name heuristic (default)
A component is treated as "yours" unless its name matches ignoreNames or ignorePatterns. Tune these to match the libraries the project uses:
<Inspector
ignoreNames={["Grid", "Box", "Stack", "MyDesignSystemPrimitive"]}
ignorePatterns={[/^Chakra/, /Provider$/]}
/>Passing either prop replaces the corresponding default set. The defaults are exported if you want to extend rather than replace them:
import { Inspector, DEFAULT_IGNORE_NAMES } from "@justuswitmer/fiber-inspector";
<Inspector ignoreNames={[...DEFAULT_IGNORE_NAMES, "MyPrimitive"]} />;Build marker (exact)
The optional Babel plugin tags every component defined in your source with Component.__appComponent = true. Bundlers do not run Babel over node_modules, so library components never get the flag. Wire the plugin into your build, then set strict:
<Inspector strict />With strict, only tagged components count and no name guessing happens. See the plugin file header for the Vite config snippet. Note: Next.js uses SWC and does not run JS Babel plugins without opting out of SWC; on Next, prefer the name heuristic.
Props
| Prop | Type | Default | Purpose |
| ---------------- | ------------------ | ------------------ | ------------------------------------------------------- |
| strict | boolean | false | Only build-marked components count as app components. |
| marker | string | "__appComponent" | Property name the Babel plugin sets on user components. |
| ignoreNames | Iterable<string> | built-in set | Component names hidden in "your components" mode. |
| ignorePatterns | RegExp[] | built-in list | Name patterns hidden in "your components" mode. |
| defaultAppOnly | boolean | true | Whether the panel opens in "your components" mode. |
Using the building blocks
The lower-level functions are exported for building custom tooling on the same Fiber-access layer:
import {
getFiberFromNode,
createClassifier,
inspect,
} from "@justuswitmer/fiber-inspector";
const fiber = getFiberFromNode(document.querySelector("#some-node")!);
const classifier = createClassifier({ strict: true });
const data = fiber && inspect(fiber, classifier, { appOnly: true, snap: true });How it works
Every DOM node React renders carries a hidden property __reactFiber$<random> pointing to its Fiber node. From a fiber you can walk the live render tree: type identifies the component (unwrap memo / forwardRef via $$typeof), memoizedProps holds current props, memoizedState holds class state or a linked list of hook records, dependencies.firstContext holds subscribed contexts, and return / child / sibling link the tree.
Hover snapping: in "your components" mode the inspector walks up via fiber.return until it reaches an app component, so hovering a <div> inside MyComponent → Grid → div → button snaps to MyComponent. In "all" mode it snaps to the nearest component fiber.
Known limitations
- Hook names. React does not expose hook variable names at runtime, only the linked list of hook records. The panel shows
hook 0,hook 1, etc. in call order. Mapping these back toconst [cartCount] = useState(2)requires source parsing, which this does not do. - Source location. React 18 exposed
fiber._debugSourcewith file and line; React 19 removed it (facebook/react#28265). The panel reads it when present and shows "not available at runtime" otherwise. Restoring editor-jump on React 19 needs a build-time JSX-source plugin plus a dev-server endpoint. - Large trees. The structure walk caps at
maxNodes = 160andmaxDepth = 8(ininspect.ts). Raise them if a real app hits the caps.
Prior art
- react-dev-inspector — in-app overlay with editor jump.
- bippy — safe Fiber access across React 17–19 by registering as a fake DevTools hook. If you want source-aware features without owning the internals-access code, leaning on
bippyfor the access layer is the better path than maintaining the direct-access utilities here.
