Stop guessing, start tracing.
Trace exactly which action moved your Zustand state — middleware, a focused Devtools UI, and one-click Vitest/Jest snippets.
- 🔍 See why state changed — not just what
- 🕒 Timeline of actions (labeled
setcalls) - 🔁 Before / after state (diff-friendly workflow in Devtools)
- 🧪 Copy real transitions as tests (Vitest / Jest)
❌ console.log debugging · guessing what flipped state
✅ Timeline · action tracing · instant test snippets
❌ “Something mutated this slice…”
✅ Named actions end-to-end
Zustand stays simple; debugging multi-action flows does not.
Redux DevTools-style tooling often shows what moved — zustand-flow is built around the flow: named actions, readable timeline, and turning that moment into a test.
npm install zustand-flowPeers: react, react-dom, zustand (^5).
1. Middleware — third argument to set is the action label:
import { create } from 'zustand'
import { flowMiddleware } from 'zustand-flow'
type Store = { count: number; increment: () => void }
export const useStore = create<Store>()(
flowMiddleware((set) => ({
count: 0,
increment: () =>
set((s) => ({ count: s.count + 1 }), false, 'increment'),
}))
)Multiple stores: flowMiddleware({ namespace: 'cart' }, (set) => ({ … })).
2. Devtools (dev only)
import { ZustandFlowDevtools } from 'zustand-flow/devtools'
function App() {
return (
<>
<YourApp />
<ZustandFlowDevtools />
</>
)
}3. Copy as test — from the panel, or in code: buildCopyTestSnippet(event, { storeId: 'useStore' }) · copy-test-snippet.ts
- zustand-flow.vercel.app — try the timeline and “copy as test” in the browser.
- Screen recording (GitHub-hosted clip).
import { flowMiddleware, useFlowEvents } from 'zustand-flow'| Import | Purpose |
|---|---|
zustand-flow |
Middleware, events, diff helpers, snippet builders, useFlowEvents |
zustand-flow/devtools |
ZustandFlowDevtools |
Exports: ESM + CJS + types for . and ./devtools. Publish the full dist/ tree (files in package.json).
Events keep references from get() before/after set. In-place mutation breaks timelines — keep updates immutable.
Recording follows process.env.NODE_ENV (with a globalThis.process fallback). The demo uses a small shim in vite.demo.config.ts so npm run dev works without a full process polyfill.
| Script | Output | Purpose |
|---|---|---|
npm run build |
dist/ |
Library ESM (.js) + CJS (.cjs) + .d.ts |
npm run build:demo |
dist-demo/ |
Demo SPA |
npm run dev |
— | Demo dev server |
npm test |
— | Vitest |
| Path | Role |
|---|---|
src/lib/ |
Middleware, event store, env, diff, snippets |
src/react/ |
use-flow-events, Devtools UI |
src/index.ts |
Main entry |
src/devtools.tsx |
zustand-flow/devtools entry |
