I spent thirteen years building mobile apps. Flutter, native Android, native iOS. For most of that time, when something rendered wrong, I did the same thing: open DevTools, click the widget, look at it.
Not the DOM node. Not the element. The widget — the thing I had actually written, with the name I had given it, holding the state I had put in it. One panel showed me the tree, its properties, the rebuild count, and the line of code that produced it.
Then I started doing production React work, and I had to relearn how to debug.
The thing nobody warns you about
The first time a React layout misbehaved on me, I did what I always do: opened the browser inspector and clicked the element.
I got a <div>.
Inside another <div>. Inside four more, one of which had a class name like
css-1x3kf9p. Somewhere in that stack was a component I had written, and nothing
on screen told me which one, or where it lived.
This is so normal to web developers that describing it feels rude. It is the water you swim in. But arriving from a decade of component-aware tooling, the gap was impossible to unsee — and the more I worked, the more I realised the problem was not that the tools were bad. Every individual tool was excellent.
The problem was that there were eight of them.
Eight tools, one bug
Here is a real debugging session, compressed. A list renders slowly and occasionally shows stale data.
- Browser DevTools — inspect the element. Get
<div>soup. No component name. - React DevTools — different panel. Find the component. It has the right props. But this panel does not know which file it came from.
- The editor — search the codebase for the component name. Two matches. Guess.
- Browser DevTools again — Network tab. Find the request. It returned correctly, so the data was fine when it arrived.
- React Profiler — a flame chart. It tells me a render was slow. It does not tell me which component re-rendered more than it should have.
console.log— because at this point I have given up and I am printing things.- The editor's debugger — set a breakpoint. It binds against the Node process. The bug is in a browser-side effect. The breakpoint never fires.
- Coverage report — separately generated, opened once, immediately forgotten.
Each step is fast. The switching is not. And the switching is where the context dies: by the time I am in tool five I am no longer holding the shape of the thing I was chasing in tool two.
What Flutter DevTools actually gets right
It is worth being precise here, because "Flutter DevTools is nice" is not an engineering argument.
What it gets right is that the component is the primitive. Not the DOM node, not the network request, not the frame. Every panel is organised around the same unit, so moving between panels does not cost you your place.
| Flutter DevTools | Typical web setup | |
|---|---|---|
| Inspect a thing | Widget, by name | <div>, unnamed |
| See its properties | Same panel | Different panel |
| Jump to its source | One click | Search the codebase |
| Rebuild counts | Per widget | Flame chart, aggregate |
| Where it runs | One window | Editor, browser, terminal |
None of these are individually impossible on the web. React DevTools gives you the tree. Source maps give you the file. The Profiler gives you timing. They simply do not know about each other, and no single one of them can tell you this component, this render, this request, this line.
The idea, stated plainly
If the component is the unit that survives every context switch, then the tool should be built around component identity — and it should live where the source already is.
That is the whole thesis. Everything else is implementation.
It also explains a design decision people ask about: why a VS Code extension rather than a browser extension. The browser has the runtime but not the source. The editor has the source and, it turns out, can be given the runtime. Only one of those two gaps is bridgeable.
Making it real: the framework problem
The moment you decide the component is the primitive, you inherit a hard problem: every framework has a different idea of what a component is at runtime.
React keeps fibers. Vue keeps component instances. Svelte compiles most of itself away. Angular has its own debug API. Solid compiles components away entirely, so at runtime there is nothing left to find.
So the agent that runs inside your app resolves one adapter per framework behind a single interface:
export interface FrameworkAdapter {
name: string
detect(): boolean
getInstanceForElement(el: HTMLElement): any
isComponentRoot?(el: HTMLElement): boolean
getSourceForInstance(instance: any): Source | undefined
getDisplayNameForInstance(instance: any): string
getPropsForInstance(instance: any): ScalarProps
updateProp(instance: any, el: HTMLElement, name: string, value: ScalarValue): boolean
}Everything above this interface — the tree, the overlays, click-to-source, the props panel — is framework-agnostic. Everything framework-specific lives below it. Four frameworks have real adapters today: React, Vue, Svelte and Angular. Anything that compiles its components away gets a structural fallback, which is honest about what it can and cannot show you.
The bug that taught me the most
detect() looks trivial. It was not.
The obvious way to detect React is to check for __REACT_DEVTOOLS_GLOBAL_HOOK__
on window. It is the documented hook. Every tutorial uses it.
It broke everything — because we inject a stub of that hook ourselves, on every page, for the Performance tab. Which meant the React adapter reported "yes, this is React" on a Vue app. And on Angular. And on Svelte.
Every framework detected as React. The tree built, the panel populated, and everything was quietly, confidently wrong.
The fix was to stop trusting a global and look for the artefacts React actually leaves on real DOM nodes:
detect() {
// Must look for REAL React fibers on the DOM — NOT
// __REACT_DEVTOOLS_GLOBAL_HOOK__, which Inspectro's proxy injects a stub of
// on every page (for the Performance tab), so it's true even for Vue/
// Angular/Svelte. Relying on it made every framework misdetect as React.
if (document.querySelector('[data-reactroot]')) return true
const els = document.querySelectorAll('body *')
const limit = Math.min(els.length, 1000)
for (let i = 0; i < limit; i++) {
for (const k of Object.keys(els[i])) {
if (
k.startsWith('__reactFiber$') ||
k.startsWith('__reactContainer$') ||
k.startsWith('__reactInternalInstance$')
) {
return true
}
}
}
return false
}That comment is still in the source, and it stays there. It is the kind of thing you only learn by having your own tool lie to you.
The lesson generalises: a debugger has to be more careful about observation than the thing it observes. We had changed the page in order to measure it, and then measured our own change. Every runtime tool eventually meets some version of this.
Why doesn't React DevTools just do this?
This is the first question anyone asks, and it deserves a straight answer rather than a dismissal. React DevTools is excellent. I use it. It gives you the component tree, the props, the hooks, and a profiler — most of what I have described wanting.
The limit is not quality. It is where it lives.
React DevTools is a browser extension, so it can see the runtime and cannot see
your repository. It can tell you that LineItem re-rendered forty times. It
cannot open src/cart/LineItem.tsx at line 42, because it has no concept of your
filesystem and no way to reach your editor.
It is also, reasonably, React-only. Vue DevTools is a separate extension with a separate UI and separate shortcuts. If your product has a React app and a Vue admin panel — ordinary in any company more than a few years old — you learn two tools and keep two mental models.
And it is scoped to components by design. It does not know which network request that component fired, what its coverage looks like on the branch you are on, or whether the accessibility tree it produces is sound. Those are all different tools again.
None of that is a criticism of React DevTools. It does its job well. It simply sits on the wrong side of the gap: it has the runtime and not the source, and an extension cannot cross that boundary. An editor extension can, which is the only real argument for building this where we built it.
What it still doesn't do
A tool essay that lists only strengths is marketing. Here is the current state, including the parts I would rather not write.
Prop editing is React-only. The component tree works across four frameworks, but editing a value live and watching the UI update works on React. Vue, Svelte and Angular are read-only today.
SolidJS gets a structural view, not a component tree. Solid compiles its components away, so at runtime there is genuinely nothing to read — no instances, no fibers, no metadata. We fall back to a DOM view and label it honestly rather than pretending. Vanilla JavaScript and web components land in the same place.
Framework coverage is uneven. React and Next.js are the deepest. Vue, Nuxt, Angular and Svelte work with caveats — some property shapes resolve less completely, and source resolution depends on what each compiler chose to leave behind.
Prop edits do not write back to source yet. You change a value, the UI updates, and the change lives only in the running app. Persisting an edit into the file is the obvious next step and is not built.
It needs your dev server. This is a development tool. It attaches to a running dev server on your machine, not to production, and that is deliberate — but it does mean it cannot help you with a bug you can only reproduce in prod.
If any of those are the thing you actually need, this will not help you yet, and I would rather you knew that from an article than from an install.
Where this goes next
The component-as-primitive idea is the easy half. The hard half is what happens when you try to hold a component tree, a network log, render counts and a live debugger in one place — across three separate runtimes that do not share memory.
That is the next article: how the agent, the extension host and the DevTools UI actually fit together, and why keeping the preview alive while execution is paused at a breakpoint turned out to be the hardest problem in the product.
If you have spent years in a tool that knew what your components were, and then
gone back to <div> soup — that gap is what we are building against.

