Runtime Inspector

A React tree tells you what exists. The runtime graph tells you what it is doing, what it reads, and what caused it to re-render — and the inspector draws that, live, at sixty frames a second.

What is in the graph

The graph is not a component tree with extra fields. It is a typed, directional graph over eleven kinds of node.

The nodes

Session · User · Screen
The run, the identity behind it, and the routes it visited. Screen nodes are what a route assertion and the journey view resolve against.
Component
Every mounted component, with its mount-time testID, role, text and source location, plus its render count and prop history. Most of what you look at.
Store · Query
Zustand stores with mutation counts, and TanStack Query entries with status and fetch counts. Both are assertable.
Gesture · Animation
Recognised gestures with normalised touch tracks, and running animations.
Network · Crash · Action
Requests as spans, captured exceptions with their stacks, and the verbs your app declared with defineAction.

The edges

This is the part that makes it a graph rather than a table:

rendered_in · owns
Structure — the parent spine the tree view lays out.
subscribes_to · reads_query
Data dependency. Which components read which store, which query feeds which screen.
caused_render · triggered · targeted
Causality. A session-level interaction id stamps every event a touch set in motion, so “this tap caused these fourteen renders” is recorded rather than inferred. targeted is the weaker spatial relation — the touch landed on this node — kept separate because a tap that hit something and did nothing is a distinct and interesting fact.
mutated · animates · preceded
State writes, animation ownership, and ordering.

Where the edges come from

None of it is configured. The adapters install themselves on import and each one contributes its own slice: React fiber commits give mounts, renders and prop changes; React Navigation gives routes; Zustand gives mutations and the components subscribed to them; gesture-handler gives touches with normalised tracks; TanStack Query, ScrollView, SQLite, AsyncStorage, MMKV and WebSocket each add their own spans. Ten adapters, none of them wired up by you — and one whose library is absent no-ops, so the graph stays valid either way.

Causality is stamped centrally rather than threaded through each adapter: a gesture opens an interaction chain and stays current for a short settle window, and the activation bus stamps that id onto everything event-driven that follows. Periodic samplers — metrics, frame rate — ride a separate raw bus and are never stamped, because a sample that happened to land inside a tap was not caused by it.

The render-flame tree

The default view. A deterministic depth-tiered tidy tree laid out from the resolved parent spine, with each node coloured by render count — green through amber to red — and pulse-ringed the moment it re-renders. Non-tree relationships (subscribes_to, reads_query, and the rest) are drawn as coloured cross-arcs over the top.

What that gets you in practice: a component that is repainting on every keystroke is a red node with an arc back to the store it subscribes to. You are looking at the cause and the effect in one picture, which is not a view any component-tree devtool can produce, because it does not know about the store.

The force-directed view

A toggle. Velocity-Verlet simulation seeded from the tree layout and left to settle, so clusters that are genuinely coupled end up near each other regardless of where they sit in the JSX. This is the exploration mode — good for “what is actually connected to what”, less good for finding a specific component.

Navigating

Pan, zoom, fit

Pan by dragging or scrolling; zoom with pinch (anchored at the focal point),

+scroll (anchored at the cursor), or the toolbar. Tap a node to zoom to it. Fit, + and frame the true node bounds rather than a guess.

Programmatic transforms apply instantly rather than tweening. That is deliberate and not laziness: the platform suspends the animation frame loop when no gesture is active, so a tween would never tick and the canvas would blank until your next drag woke it. Pinch stays smooth because the gesture itself drives every frame.

The graph only re-lays-out when its change signature moves, so an idle app costs nothing to display.

From a node to its source line

With the Babel plugin installed, every component carries the file, line and column where it is declared, so a node in the graph is one step from the code that produced it. Without the plugin the inspector still works — it shows names, and the source selector has nothing to match on.

The on-device overlay

The same graph, without a desktop app or a network: drop <SquiggleOverlay /> in (or use <Squiggle>, which includes it) and a floating badge expands into a live panel sourced over an in-process loopback.

○ ComponentName ↻12 s
A component, its render count, and why it last rendered — state, props or forced.
▣ storeName △8 [key, …]
A store, its mutation count, and the keys that last changed.
← Comp, Comp2
The components subscribed to that store — the subscribes_to edges, rendered inline.
file.tsx:42
The source location, when the Babel plugin is installed.

It self-gates: nothing renders outside dev, on the web, or when a WebSocket consumer is configured — the protocol is single-consumer, so the overlay stands down when the desktop or the CLI is attached. Leaving it mounted in a production tree is safe.

The Perf tab

Renders per second, plotted live, with a draggable crosshair that scrubs into the past — so a spike you noticed a moment ago is still inspectable. This is the fast read on “is this screen quiet when nothing is happening”, which is the question a render storm fails.

Arriving from an insight

A performance finding names the component it blames. Clicking its evidence chip in the Performance Center navigates here with that node focused — the insight-highlight path. If the recorded node is not in the live graph any more, you get an inline notice saying so rather than an empty canvas or a crash.

Querying it instead

Everything the inspector draws is also a query. The graph is projected into SQLite as it streams, so the same facts are available without a UI — which is how the CLI and agents read them:

Terminal
# every component that re-rendered more than 20 timessquiggle perf sql "  select node_id, count(*) as renders  from renders  where session_id = (select id from sessions order by started_at desc limit 1)  group by node_id having renders > 20 order by renders desc"

An agent gets the same thing structurally through squiggle_query over the live graph, or squiggle_exec to filter server-side and return only what it needs. See Agents & MCP.