DEV Community

Kuba Opoczka
Kuba Opoczka

Posted on

I built a time-travel debugger for Zustand — and it caught three bugs I'd already shipped

If you use Zustand, you've had this moment: the UI is wrong, you know a store changed when it shouldn't have, and now you're scattering console.log calls and refreshing, trying to catch the one action that did it. State debugging is detective work — and Zustand, by design, is so minimal that it doesn't give you much to work with.

I wanted to see it instead. So I built Zustand DevTools: a Chrome DevTools panel that records every state change in your app and lets you walk back through them.

The problem

Zustand's whole appeal is that it's tiny and unopinionated — no boilerplate, no context wrappers, no ceremony. The flip side is there's no built-in answer to "what changed, when, and because of which action?" The Redux DevTools middleware exists, but it's Redux-shaped: action-centric, and awkward the moment you have several small stores instead of one big one.

What I actually wanted while debugging was boring and specific:

  • a live view of each store's current state,
  • a timeline of every change, labelled with the action name,
  • the exact path that changed (items[1].quantity: 1 → 2), not a whole-object diff I have to eyeball,
  • and the ability to jump back to any point and look around.

What it does

Two parts.

Stores + Timeline (free). Open DevTools, click the Zustand tab, and every registered store shows up live. Every set() becomes a timeline entry with the action name and a path-level diff:

[cart] addItem    items[1].quantity: 1 → 2    total: 168 → 297
Enter fullscreen mode Exit fullscreen mode

Click any entry to time-travel to that moment — safely, by ID, rather than by replaying a fragile sequence of actions.

Trace Sessions (Pro). Record while you reproduce a bug, then inspect the recording: path-level diffs, the likely call-site of each change, compare any two entries, and export a redacted session a teammate can import and inspect view-only. That export is the part I use most. "Here's the recording — drag the playhead to step 4 and you'll see where the total goes wrong" beats a paragraph of Slack every single time.

How you wire it up

One wrapper per store you want to inspect:

import { create } from 'zustand';
import { withDevtoolsBridge } from 'zustand-devtools-bridge';

const useCartStore = create(
  withDevtoolsBridge(
    (set) => ({
      items: [],
      addItem: (item) =>
        set((s) => ({ items: [...s.items, item] }), false, 'addItem'), // named in the log
    }),
    { name: 'cart' }
  )
);
Enter fullscreen mode Exit fullscreen mode

Register a store once with a stable name and you get an accurate live view of it from then on. (There's also a zero-setup experimental view that reads hooks directly for a quick look — it's clearly labelled, because it can't always tell Zustand state apart from other hooks.)

Two design decisions I care about:

  • It chains the DevTools hook instead of clobbering it. The real React DevTools keeps working right next to it, whichever loads first. A debugging tool that breaks your other debugging tools is worse than nothing.
  • State never leaves your machine. Data flows from the inspected page to your panel and nowhere else — no server, no analytics, no telemetry. Your app state is often the most sensitive thing on the page; it shouldn't phone home just so you can look at it.

The honest part

I build software by directing AI coding agents, and I don't trust their output by default. The architecture, the naming, the test strategy and the review are mine. There are 96 automated tests across two major framework versions, because a state tool that's subtly wrong is actively dangerous — you'd end up debugging the debugger.

And to be straight: after I'd "finished," I did a dedicated review pass over my own code and found three silent failures a green test suite had missed. I fixed them and kept the diffs. If a tool for catching bugs can't survive its own author looking hard at it, it hasn't earned your trust.

Try it

  • Free: install the Stores + Timeline tools from the Chrome Web Store, add zustand-devtools-bridge to your app, and wrap a store.
  • Pro: unlimited Trace Sessions for a €9.99 one-time purchase — three full previews free, no subscription, up to 5 devices, 30-day refund.

👉 Try it: https://kubaopoczka.github.io/zustand-devtools-site/

If you use Zustand and React, I'd genuinely like to know where it falls short — what you tried to inspect that it couldn't show you. That's the feedback that makes it better.


P.S. — I'm new here: this is my first dev.to post, and honestly still finding my feet sharing work out loud like this. If you've got tips for a newcomer (or thoughts on the tool itself), I'd genuinely love to hear them. 🙏

Top comments (0)