---
title: Devtools
description: See why a component re-rendered and what it depends on — a live render trace, an atom dependency graph, and the query store — plus a Redux DevTools bridge. Dev-only, zero production cost.
group: Core
order: 10
---

Debugging *why React re-rendered* is notoriously hard — and harder when an agent is writing the code.
Because effract drives every component through one interpreter, it already sees every `yield*`, so it can
show you what a render did and why — no `console.log` archaeology. The tooling is **dev-only and
tree-shakes away**: the instrumentation is a single connected-check, so a production build pays nothing.

## `<Inspector />` — the render trace, the dependency graph, the query store

Drop the `<Inspector />` anywhere in a dev build. It is a **plain React component** — place it as JSX (not
`{yield* …}`), alongside your app:

```tsx
import { Inspector } from '@tmonier/effract';

function DevPanel() {
  return import.meta.env.DEV ? <Inspector /> : null;
}
```

It shows, live:

- **Render trace** — for each REC render, what it yielded (services, atoms, queries, hooks, children) **and
  the attributed trigger**: `Cart: items · total — triggered by atom items`. Every render is attributed to
  one of five causes — **atom** write, **query** settle, **props** change, lifted-**hook** state, or
  **parent** render — so the two questions render-debugging actually needs, *what does this component
  depend on* and *why did it just render*, are answered per render.
- **Atom dependency graph** — every derived atom and its sources (`total ← items`), so a `derive` /
  `derive.effect` chain is visible as a graph, not guessed at.
- **Query store** — every keyed `query`: its key, status, in-flight claims, `staleTime`, and invalidation
  tags — the runtime cache, laid bare.

Because state and the logic over it live in [services](/docs/runtime/) — not scattered across
`useState`/`useEffect` — the trace is meaningful: a re-render maps to a **named atom that changed**, not an
opaque hook. That is the point of keeping logic in Effect.

`<Inspector interval={500} />` sets the query-store poll interval (ms).

## `connectReduxDevtools()` — a timeline of every atom write

For time-travel and a full action timeline, bridge atom writes to the
[Redux DevTools](https://github.com/reduxjs/redux-devtools) browser extension. Every atom is already
named — `atom(label, initial)` — so the actions read well by construction; call it once at startup:

```tsx
import { atom, connectReduxDevtools } from '@tmonier/effract';

const count = atom('counter', 0); // the label is the action label

connectReduxDevtools({ name: 'my app' }); // returns a disconnect function
// every count.set(n) / count.update(...) now appears as `counter.set` / `counter.update`
```

It is **safe to call unconditionally**: when the extension isn't installed it returns a no-op disconnect
and does nothing. It has **zero dependency on Redux** — it only speaks the extension's protocol.

## `readRenderLedger()` — ask "why did this render?" without the Inspector

In development the render ledger is always on — attribution does not wait for a connected inspector. The
last 200 renders, each with its yields and its attributed trigger, are readable from a test or a REPL:

```tsx
import { readRenderLedger } from '@tmonier/effract';

const renders = readRenderLedger(); // ReadonlyArray<RenderEvent>, oldest first
renders.at(-1)?.trigger; // { kind: 'atom', label: 'items' } — or query / props / hook / parent
```

Always empty in production. The same dev seam also reports two things worth staying visible: every
[`.hot()`](/docs/render-control/) subtree's render frequency (`hot:frequency` — an acknowledged hot spot
is never silent), and a **sync↔async flip** (`invariant:flip`) — a yielded effect that resolved
synchronously on one render and asynchronously on the next, which breaks the Rules of Hooks; wrap such
an effect in `suspend()`/`query()`.

## Handled defects stay visible

A [`.catchDefect`](/docs/errors/) fallback absorbing a defect would be invisible exactly when you need
to see it — so in development each absorption is emitted on the same seam (`kind: 'defect:handled'`,
with the failing REC's name and the pretty-printed `Cause`). effract itself never writes to the console;
connect a consumer and nothing your fallbacks swallow goes unseen.

## Zero production cost

Both consumers hang off one instrumentation seam. When nothing is connected — the normal production case —
the core does a single `connected?` check and builds no events; the `<Inspector />` and
`connectReduxDevtools` code itself **tree-shakes away** because you didn't import it. You pay for the
devtools only in the dev build where you use them.
