//
Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Wrap your store with the devtools middleware to connect to the Redux DevTools browser extension. Inspect state, track actions, and use time-travel debugging.
import { create } from "zustand";
import { devtools } from "zustand/middleware";
interface AppStore {
count: number;
increment: () => void;
decrement: () => void;
}
export const useAppStore = create<AppStore>()(
devtools(
(set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 }), false, "increment"),
decrement: () => set((s) => ({ count: s.count - 1 }), false, "decrement"),
}),
{ name: "AppStore" }
)
);// stores/order-store.ts
import { create } from "zustand";
import { devtools } from "zustand/middleware";
interface OrderItem {
id: string;
name: string;
quantity: number;
price: number;
}
interface OrderStore {
items: OrderItem[];
status: "idle" | "processing" | "completed" | "error";
discount: number;
addItem: (item: Omit<OrderItem, "quantity">) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
applyDiscount: (percent: number) => void;
submitOrder: () => Promise<void>;
resetOrder: () => void;
}
export const useOrderStore = create<OrderStore>()(
devtools(
(set, get) => ({
items: [],
status: "idle",
discount: 0,
addItem: (item) =>
set(
(s) => {
const existing = s.items.find((i) => i.id === item.id);
if (existing) {
return {
items: s.items.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
};
}
return { items: [...s.items, { ...item, quantity: 1 }] };
},
false,
{ type: "order/addItem", item } // Named action with payload
),
removeItem: (id) =>
set(
(s) => ({ items: s.items.filter((i) => i.id !== id) }),
false,
{ type: "order/removeItem", id }
),
updateQuantity: (id, quantity) =>
set(
(s) => ({
items: s.items.map((i) => (i.id === id ? { ...i, quantity } : i)),
}),
false,
{ type: "order/updateQuantity", id, quantity }
),
applyDiscount: (percent) =>
set(
{ discount: Math.min(100, Math.max(0, percent)) },
false,
{ type: "order/applyDiscount", percent }
),
submitOrder: async () => {
set({ status: "processing" }, false, "order/submitOrder/pending");
try {
const { items, discount } = get();
await fetch("/api/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items, discount }),
});
set(
{ status: "completed", items: [], discount: 0 },
false,
"order/submitOrder/fulfilled"
);
} catch {
set({ status: "error" }, false, "order/submitOrder/rejected");
}
},
resetOrder: () =>
set(
{ items: [], status: "idle", discount: 0 },
false,
"order/reset"
),
}),
{
name: "OrderStore",
enabled: process.env.NODE_ENV === "development",
}
)
);// components/order-form.tsx
"use client";
import { useOrderStore } from "@/stores/order-store";
export function OrderForm() {
const items = useOrderStore((s) => s.items);
const status = useOrderStore((s) => s.status);
const discount = useOrderStore((s) => s.discount);
const addItem = useOrderStore((s) => s.addItem);
const removeItem = useOrderStore((s) => s.removeItem);
const updateQuantity = useOrderStore((s) => s.updateQuantity);
const applyDiscount = useOrderStore((s) => s.applyDiscount);
const submitOrder = useOrderStore((s) => s.submitOrder);
const subtotal = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
const total = subtotal * (1 - discount / 100);
return (
<div>
<h2>Order ({status})</h2>
<button
onClick={() => addItem({ id: "p1", name: "Widget", price: 9.99 })}
>
Add Widget
</button>
<ul>
{items.map((item) => (
<li key={item.id}>
{item.name} x{item.quantity} = ${(item.price * item.quantity).toFixed(2)}
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}>+</button>
<button onClick={() => removeItem(item.id)}>Remove</button>
</li>
))}
</ul>
<div>
<label>
Discount %:
<input
type="number"
value={discount}
onChange={(e) => applyDiscount(Number(e.target.value))}
/>
</label>
</div>
<p>Total: ${total.toFixed(2)}</p>
<button onClick={submitOrder} disabled={status === "processing"}>
{status === "processing" ? "Submitting..." : "Submit Order"}
</button>
</div>
);
}devtools middleware intercepts every set call and sends the state diff plus an action name to the Redux DevTools extension.set(state, replace, actionName) names the action in DevTools. Without it, actions show as "anonymous.""increment") or objects ({ type: "order/addItem", payload }) for richer debugging info.name option in devtools config sets the store name shown in the DevTools instance selector.setState.enabled option controls whether DevTools connects. Set it to false in production to avoid overhead.Multiple stores in DevTools:
// Each store gets its own DevTools instance
const useAuthStore = create<AuthState>()(
devtools((set) => ({ ... }), { name: "AuthStore" })
);
const useCartStore = create<CartState>()(
devtools((set) => ({ ... }), { name: "CartStore" })
);
// Both appear as separate instances in Redux DevToolsConnecting to a remote DevTools server:
devtools(storeCreator, {
name: "RemoteStore",
// Connect to remote Redux DevTools instance
// Useful for debugging mobile or embedded apps
});Trace option for stack traces:
devtools(storeCreator, {
name: "Store",
trace: true, // Show stack trace for each action
traceLimit: 25, // Max stack frames to show
});With other middleware:
// devtools should always be the outermost middleware
const useStore = create<State>()(
devtools(
persist(
immer((set) => ({ ... })),
{ name: "persist-key" }
),
{ name: "DevToolsName" }
)
);devtools middleware does not change the store type. All types are preserved.set is typed as string | { type: string; [key: string]: unknown }.// Fully typed set with action name
set(
(state) => ({ count: state.count + 1 }), // State updater
false, // replace flag
{ type: "counter/increment", by: 1 } // Action for DevTools
);devtools is not the outermost middleware, the DevTools may not capture all state changes correctly. Always wrap everything else inside devtools.set (action name) only works when devtools middleware is active. Without it, the third argument is silently ignored.enabled: false in production.serialize option to filter or limit what is sent.| Approach | Pros | Cons |
|---|---|---|
| Redux DevTools middleware | Time-travel, visual state inspection | Requires browser extension, production overhead |
| Console logging middleware | No dependencies, simple | No visual UI, no time-travel |
| React DevTools | Built-in, shows component state | Cannot inspect Zustand stores directly |
| Custom debug panel | Tailored to your app | Must build and maintain it |
set(
(s) => ({ count: s.count + 1 }),
false,
"increment" // Action name shown in DevTools
);set.{ type: "order/addItem", payload }.setState.devtools(storeCreator, {
name: "AppStore",
enabled: process.env.NODE_ENV === "development",
});devtools is not the outermost middleware, it may not capture all state changes correctly.devtools(persist(immer((set) => ({ ... })))).devtools middleware is active.serialize option to filter what is sent.enabled: false.devtools(storeCreator, {
name: "Store",
trace: true,
traceLimit: 25,
});trace option adds a stack trace to each action in DevTools.string | { type: string; [key: string]: unknown }.devtools middleware does not change the store's type signature.set(
(state) => ({ count: state.count + 1 }),
false,
{ type: "counter/increment", by: 1 }
);Reviewed by Chris St. John·Last updated Jul 7, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥