Sub-skill of tanstack. Owns the dev-only, opt-in debugging panel that ships with TanStack: a floating host (@tanstack/react-devtools) that other devtools (router today, more later) plug into, plus the Vite plugin (@tanstack/devtools-vite) that injects the runtime which makes "click an element in the browser → jump to its source file" work. The point: tighten the iPad-over-LAN debug loop when explicitly requested — see what's broken, click straight to the source line.
When to invoke
- Adding a new TanStack devtool surface (a Query devtool, a Form devtool, etc.) — they all plug into
<TanStackDevtools>as aplugins[]entry. - Diagnosing why the floating devtool button isn't showing in
bun run dev. - Confirming the production bundle has zero devtool residue (the Pillar 4 / iPad-LAN bundle hygiene check).
- Understanding the lazy + DEV-gate pattern dean-stack uses to keep devtools out of
dist/. - Anyone proposing
click-to-react-component,@locator/runtime, or another browser-→-source tool —@tanstack/devtools-vitealready does this. Don't add a competitor.
Owns
The dev-only mount of <TanStackDevtools> in apps/web/app/lib/root-shell.tsx, the opt-in tanstackDevtools() Vite plugin in apps/web/vite.config.ts, the import.meta.env.DEV + VITE_ENABLE_TANSTACK_DEVTOOLS + lazy() tree-shaking pattern, and the policy that all three devtool packages stay in devDependencies.
Defers to
tanstack(parent) — version pin and dispatch.tanstack-router-routing— the<TanStackRouterDevtoolsPanel />reads its data from the router instance authored there.react-19-primitives—lazy()+<Suspense fallback={null}>is the React-19 idiom that makes the dev gate render-safe.react-compiler-rules— the side-channel rule applies: the devtool host mounts once at the root boundary, never inside a render function.
Dean-stack rules
- Devtools are opt-in in dev, and absent in production. The default
bun run devpage must stay close to production behavior: no root devtools host, no@tanstack/devtools-viteDOM source annotations, and no sidecar event-bus unlessVITE_ENABLE_TANSTACK_DEVTOOLS=trueis set. The pattern inapps/web/app/lib/root-shell.tsxis:
Vite's static-replacement ofconst TanStackDevtools = import.meta.env.DEV && import.meta.env.VITE_ENABLE_TANSTACK_DEVTOOLS === "true" ? lazy(async () => { ... }) : null;import.meta.env.DEVlets dead-code elimination drop the entirelazy()branch (and its dynamic imports) from the production bundle. The env flag prevents default dev-load freezes from source-link instrumentation. Verify withgrep -r '@tanstack/react-devtools' apps/web/dist/client/assets/— must return zero hits. @tanstack/devtools-viteis a Vite plugin, not a runtime, and it is opt-in. It belongs invite.config.tsbehind the sameVITE_ENABLE_TANSTACK_DEVTOOLS === "true"guard. The plugin annotates DOM nodes with source metadata and starts source-linking machinery duringvite dev; do not enable it by default.- Devtool components are NOT dean-stack components and do NOT need a Storybook story (Pillar 1 exemption). They're third-party panels; the "no component without a story" rule applies to components authored under
apps/<name>/app/components/, not to dev-time host panels mounted at the root. - Devtools introduce zero app-level state (Pillar 3). Their internal panel-state (open/closed, active tab) lives in the devtool's own scope; never persist any of it through
atomWithIDB. - Pillar 4 (CLI-gate-first): all three packages are
devDependencies, neverdependencies. The gate enforces this by ensuring the production bundle stays free of devtool symbols. VITE_ENABLE_TANSTACK_DEVTOOLS=true bun run devis when devtools live. Plainbun run devkeeps devtools off.bun run preview(which Playwright app tests usually drive) runs the production build, so devtools are absent there. That's correct — app tests assert app behavior, not devtool UI.- The headliner is browser→source linking.
@tanstack/devtools-vite'sdevtools()plugin is the reason this skill exists: clicking a rendered element in the running browser opens the source file at the exact line in your editor. That's the iPad-LAN debug payoff.
Patterns
vite.config.ts — opt into the plugin first
// apps/web/vite.config.ts
import { devtools as tanstackDevtools } from "@tanstack/devtools-vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
function enableTanStackDevtools(): boolean {
return process.env.VITE_ENABLE_TANSTACK_DEVTOOLS === "true";
}
export default defineConfig({
plugins: [
...(enableTanStackDevtools() ? tanstackDevtools() : []),
...sharedPlugins(),
tanstackStart({ /* ... */ }),
],
});
Keep the conditional plugin first when enabled. The plugin is a no-op for vite build, but it still changes the dev browser runtime, so it must remain opt-in.
root-shell.tsx — lazy + DEV/env-gate the host
import { type ReactNode, Suspense, lazy } from "react";
const TanStackDevtools =
import.meta.env.DEV && import.meta.env.VITE_ENABLE_TANSTACK_DEVTOOLS === "true"
? lazy(async () => {
const [{ TanStackDevtools: Host }, { TanStackRouterDevtoolsPanel }] = await Promise.all([
import("@tanstack/react-devtools"),
import("@tanstack/react-router-devtools"),
]);
return {
default: () => (
<Host
config={{ position: "bottom-right" }}
plugins={[{ name: "TanStack Router", render: <TanStackRouterDevtoolsPanel /> }]}
/>
),
};
})
: null;
function RootComponent(): ReactNode {
return (
<html lang="en">
<head>{/* ... */}</head>
<body>
{/* ... app content ... */}
{TanStackDevtools ? (
<Suspense fallback={null}>
<TanStackDevtools />
</Suspense>
) : null}
<Scripts />
</body>
</html>
);
}
Why this shape: production sees null → the {TanStackDevtools ? ... : null} JSX becomes null → the dynamic imports never get bundled. Default dev also sees null unless the opt-in flag is set, keeping source-link instrumentation away from normal game iteration.
Adding a new TanStack devtool plugin
When a new package ships (e.g. a Query devtool, a Form devtool), it joins the existing plugins[] array on the host:
plugins={[
{ name: "TanStack Router", render: <TanStackRouterDevtoolsPanel /> },
{ name: "TanStack Query", render: <TanStackQueryDevtoolsPanel /> }, // future, hypothetical
]}
Not relevant for dean-stack today (no Query — no server, IDB-first).
Verifying zero production residue
bun run build
grep -r '@tanstack/react-devtools' apps/web/dist/client/assets/ # zero hits
grep -r 'TanStackRouterDevtoolsPanel' apps/web/dist/client/assets/ # zero hits
grep -r 'devtools-vite' apps/web/dist/client/assets/ # zero hits
If any return hits, the DEV gate isn't tree-shaking — investigate before merging.
Anti-patterns
- Don't enable TanStack Devtools by default. Both the Vite plugin and the React host require
VITE_ENABLE_TANSTACK_DEVTOOLS=true. Always-on devtools can inject source metadata into the live DOM and stall default dev loads. - Don't import the devtool packages at the top of
__root.tsxorroot-shell.tsxwithout the DEV/env gate. Static imports always end up in the bundle; the lazy + ternary is the only safe shape. - Don't put the devtool packages in
dependencies. They're dev tools —devDependenciesonly. The gate doesn't catch this on its own; reviewers must. - Don't add
click-to-react-component,@locator/runtime, or another browser-→-source tool.@tanstack/devtools-vitealready provides this. A second one is duplicate weight innode_modulesand a new place for source-link logic to drift. - Don't mount
<TanStackDevtools>inside a route component or anywhere besides the root. Mounting it deeper means it gets unmounted on navigation and loses its panel state. - Don't write a Storybook story for the devtool host (Pillar 1 exemption). Devtools aren't a dean-stack component; they're a third-party panel whose UI is already tested upstream.
- Don't reach for
useEffectto mount the host. Thelazy()+ render shape is the right one —useEffectintroduces a one-frame mount delay and risks the side-channel rule (devtool mutates DOM). - Don't add
<TanStackRouterDevtoolsPanel />outside of the host'splugins[]array. It's a panel, not a standalone overlay; mounting it bare loses the docked-panel UX.
Triggers on
TanStackDevtools, TanStackRouterDevtoolsPanel, devtools-vite, tanstack devtools, click to source, browser to editor, router devtools, dev panel