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.

  1. Browser DevTools — inspect the element. Get <div> soup. No component name.
  2. React DevTools — different panel. Find the component. It has the right props. But this panel does not know which file it came from.
  3. The editor — search the codebase for the component name. Two matches. Guess.
  4. Browser DevTools again — Network tab. Find the request. It returned correctly, so the data was fine when it arrived.
  5. 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.
  6. console.log — because at this point I have given up and I am printing things.
  7. 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.
  8. 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.

DOMComponentsNetworkProfilerConsoleDebuggerCoverageA11yno shared context — you carry it between themone runtime surface — component identity carried through all of itThe unit that survives every switch is the component. So make it the primitive.
The same bug, seen by eight tools that share nothing.

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 DevToolsTypical web setup
Inspect a thingWidget, by name<div>, unnamed
See its propertiesSame panelDifferent panel
Jump to its sourceOne clickSearch the codebase
Rebuild countsPer widgetFlame chart, aggregate
Where it runsOne windowEditor, 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
}
Tree · overlays · click-to-source · props panelframework-agnostic — written onceinterface FrameworkAdapterdetect() · getInstanceForElement() · getSourceForInstance() · updateProp()ReactfibersVueinstancesSveltecompiler metaAngulardebug APIstructuralDOM onlySolid, vanilla JS and web components land here — they compile their components away,so at runtime there is no component model left to read.
One interface above, four framework adapters below — and an honest fallback for the frameworks that leave nothing to read.

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.