react-parent-owner-trees
v0.1.19
Published
Live parent-tree and owner-tree diagrams of a running React app, rendered as indented SVG trees with owner arcs, context areas, and boundary regions
Downloads
294
Maintainers
Readme
react-parent-owner-trees
Live diagrams of a running React app, in the notation from A Visual Notation for React's Parent and Owner Trees. The library draws two SVG trees:
- Parent tree: where each component's output lands in the markup.
- Owner tree: which component rendered which. An element's owner is the component whose JSX created it, so owner edges show where props come from.
None of this structure (which component owns which, where props come from, what a boundary scopes) lives in a single file, so reading it from code means reconstructing it in working memory. A picture can hold it for you. The post draws that picture by hand; this library draws it from your app.
Built on bippy for fiber access.

The demo app as its two trees. In the parent tree, Stats and the
Posts sit inside Frame's markup, but the dashed arcs show DemoApp
owns them. The owner tree on the right makes that the structure:
slot composition, exactly where the two trees diverge.
Live extraction needs a development build of React 19, because production builds strip owner information. The static walker (below) also works in production builds and at build time.
[!NOTE] Alpha. This is an early release. The API may change between 0.x versions, and live extraction has not been stress-tested on large apps yet. Issues and reports are welcome.
Install
Requires React 19 (the react peer range is >=19 <20). Live
extraction additionally needs a development build of React; the static
walker also works with production React.
npm install react-parent-owner-treesbippy (the fiber-access layer) is a
peer dependency that npm and pnpm install automatically. Only the
/extract entry uses it, and the script tag below needs no install at
all. With Yarn, add bippy yourself if you import /extract.
Quick start: script tag
The standalone build attaches a tree panel to any dev-mode React app. It is
one self-contained file: bippy is bundled inside, so there is nothing to
install. Load it as a classic script in <head>, before your app
bundle:
<script src="https://unpkg.com/react-parent-owner-trees/dist/auto.global.js"></script>(Or serve dist/auto.global.js from the package yourself.)
It adds a fixed panel with both trees, updated on every commit, plus a
hide button and a badge to bring the panel back. On a large app, scope
the view down: select a row, click scope to, and both trees narrow to
that subtree. A breadcrumb in the header zooms back out. The script must load
first so bippy can install the React DevTools hook before react-dom
initializes.
Quick start: in your own page
The package has three entry points:
| Entry | What it gives you | Dependencies |
| --- | --- | --- |
| react-parent-owner-trees/core | createTreeRenderer and the InputNode type | none |
| react-parent-owner-trees/extract | startExtraction: live fiber extraction, one tree per commit | bippy, react (dev build) |
| react-parent-owner-trees/static | elementToTree: element walker, no DOM needed | react |
Wire extract to core like this:
// Import order matters: extract must be imported before react-dom,
// anywhere in your module graph. See "Import order" below.
import { startExtraction, contextName } from "react-parent-owner-trees/extract";
import { createTreeRenderer } from "react-parent-owner-trees/core";
const renderer = createTreeRenderer({ sizing: "fixed", contextName });
startExtraction((roots, diff, rendered) => {
document.getElementById("parent")!.innerHTML = renderer.parentTreeSvg(
roots,
rendered,
);
document.getElementById("owner")!.innerHTML = renderer.ownerTreeSvg(
roots,
rendered,
);
});startExtraction calls you back on every React commit with an
InputNode[], a TreeDiff against the previous snapshot, and the set of
node ids that actually performed work in the commit (bailed-out
reconciliation visits don't count). Pass that set to the SVG methods and
the rows that rendered blink once, subtly: ambient liveness in the
Observable-minimap sense. It's per-call data, not renderer state: pass it
only on commit-triggered renders, so your own interaction re-renders
don't replay the blink. The renderer turns it all into SVG strings; you
put them in the DOM.
Page highlighting
The extract entry also ships the selection's second surface: while a row
is focused, outline the DOM region its output governs in the running app.
getNodeHostElements(id) resolves a row to the elements behind it (the
nearest host descendants for a component, the node itself for a host row;
empty for rows with nothing on the page), and createPageHighlighter()
draws quiet fixed-position outlines that never affect page layout and
track scroll and resize. Re-resolve after every render so commits that
remount elements keep the outline honest:
import {
createPageHighlighter,
getNodeHostElements,
} from "react-parent-owner-trees/extract";
const highlighter = createPageHighlighter();
function render() {
// ... innerHTML = renderer.parentTreeSvg(...) etc ...
const selected = renderer.selectedId();
highlighter.highlight(selected ? getNodeHostElements(selected) : []);
}Import order
bippy installs the React DevTools hook as a side effect of being imported.
React checks for that hook exactly once, when react-dom first evaluates.
If the hook is not there yet, extraction stays silent forever. So the
/extract entry (or the script tag) must load before anything that imports
react-dom:
import "react-parent-owner-trees/extract"; // first
import { createRoot } from "react-dom/client"; // afterIf the order is wrong, startExtraction() logs a console.error
explaining the problem after a short grace period.
Static figures
elementToTree walks a React element tree without rendering it to the DOM.
It calls your component functions with a mock hooks dispatcher, so it works
at build time, in tests, and with production React:
import { elementToTree, contextName } from "react-parent-owner-trees/static";
import { createTreeRenderer } from "react-parent-owner-trees/core";
const roots = elementToTree(<App />);
const renderer = createTreeRenderer({ sizing: "content", contextName });
const svg = renderer.parentTreeSvg(roots);Options: elementToTree(root, prefix, { rsc, suspended, errored }).
rsc treats the tree as a server-component program (mark client entry
points with markUseClient), suspended draws every Suspense boundary
showing its fallback, and errored forces every error boundary into its
caught state. Useful for drawing figures of states that are hard to catch
live.
Renderer options
createTreeRenderer(options) returns a renderer instance that owns all
interactive state (selection, folds, active areas), so several diagrams on
one page never share toggles.
| Option | Default | Meaning |
| --- | --- | --- |
| sizing | "content" | "content" sizes the SVG to the widest row (figures); "fixed" keeps a stable width (live tool, where rows churn every commit) |
| select | on | click a node to focus its neighborhood; click again or press Escape to clear |
| collapse | off | disclosure triangles and double-click subtree collapse |
| hooks | off | per-component hook strips, one glyph per hook call |
| serverRegions | off | tinted regions behind server-component rows |
| sourceNav | off | data-node attributes and a cmd-click source hint |
| serverColor | a teal (hsl(191 65% 40%)) | the "came from the server" color |
| contextName | — | resolves a context id to a display name; pass the contextName export of whichever producer you use |
Dark mode
The diagrams render correctly on any page out of the box. The four neutral colors (background, three text weights) read from CSS custom properties with light-mode fallbacks, so a dark page can flip them:
:root {
--rpot-bg: #1a1a1a;
--rpot-text: #ededed;
--rpot-text-light: #b5b5b5;
--rpot-text-lighter: #757575;
}This is deliberately the whole theming surface. The accent colors (state dots, memo discs, context palette, boundary colors) are part of the notation and stay fixed.
Interaction
The renderer returns plain SVG strings and never touches the DOM. Interactivity is wired by you: listen for clicks on your container, find the closest element with one of these data attributes, call the matching renderer method, then re-render both trees.
| Data attribute | Renderer method |
| --- | --- |
| data-select-toggle | toggleSelect(id) |
| data-fold-toggle | toggleFold(id) |
| data-collapse-toggle | toggleCollapse(id) |
| data-context-toggle | toggleContext(id) |
| data-boundary-toggle | toggleBoundary(id) |
| data-memo-toggle | toggleMemo(id) |
| data-hooks-toggle | toggleHooks(id) |
There are also direct setters: setArcStyle, setHooksMode, setMarks,
setView (hide DOM rows or structural rows), clearSelect, and
set*Active variants of the toggles. See the TreeRenderer type for the
full interface.
The core entry also exports small helpers for working with trees outside
the renderer: scopeToSubtree (narrow the view to one node's subtree, so
a large app can render just the region you're working on instead of the
whole tree), applyView/hideDom/hideStructural (filter a tree the same way
setView does), diffSnapshots (the TreeDiff between two snapshots),
and serializeTree. Apply them between the extraction callback and the
SVG methods:
startExtraction((roots, diff, rendered) => {
const view = scopeToSubtree(roots, scopedId);
parentEl.innerHTML = renderer.parentTreeSvg(view, rendered);
ownerEl.innerHTML = renderer.ownerTreeSvg(view, rendered);
});If the scoped id disappears (the component unmounted), the scope dissolves to the full tree instead of blanking the panel.
What the diagrams show
Every mark in the diagrams (circles, owner arcs, state dots, memo discs, context areas, boundary squares, server rows, folds) is summarized in the notation overview, with links into the full walkthrough post for each one.
Known limitations
- Live extraction works only with development builds of React 19.
- If a selected node's owner sits inside a folded run, that owner arc is not drawn (unfolding on selection is planned).
Development
npm install
npm run dev # demo app + live tool
npm test # renderer behavior + unit tests
npm run build:lib && npm run build:autoThe library lives in src/lib/; src/main.tsx and src/demo/ are the
dev app the tool observes. Design decisions are recorded in docs/adr/,
the domain vocabulary in CONTEXT.md.
License
MIT
