React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

zustanddevtoolsreduxdebugging

Redux DevTools Integration

Recipe

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" }
  )
);

Working Example

// 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>
  );
}

Deep Dive

How It Works

  • The devtools middleware intercepts every set call and sends the state diff plus an action name to the Redux DevTools extension.
  • The third argument to set(state, replace, actionName) names the action in DevTools. Without it, actions show as "anonymous."
  • Action names can be strings ("increment") or objects ({ type: "order/addItem", payload }) for richer debugging info.
  • The name option in devtools config sets the store name shown in the DevTools instance selector.
  • Time-travel debugging lets you jump to any previous state. Zustand replays state changes through setState.
  • The enabled option controls whether DevTools connects. Set it to false in production to avoid overhead.

Variations

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 DevTools

Connecting 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" }
  )
);

TypeScript Notes

  • The devtools middleware does not change the store type. All types are preserved.
  • The action name parameter on 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
);

Gotchas

  • If devtools is not the outermost middleware, the DevTools may not capture all state changes correctly. Always wrap everything else inside devtools.
  • The third argument to set (action name) only works when devtools middleware is active. Without it, the third argument is silently ignored.
  • DevTools adds runtime overhead: serializing state on every update. Disable with enabled: false in production.
  • Time-travel debugging replaces the entire state. If your store has side effects in subscriptions, time-traveling may trigger them unexpectedly.
  • Very large state trees can slow down DevTools. Use the serialize option to filter or limit what is sent.
  • The Redux DevTools browser extension must be installed for devtools middleware to have any effect. Without it, the middleware is a no-op.

Alternatives

ApproachProsCons
Redux DevTools middlewareTime-travel, visual state inspectionRequires browser extension, production overhead
Console logging middlewareNo dependencies, simpleNo visual UI, no time-travel
React DevToolsBuilt-in, shows component stateCannot inspect Zustand stores directly
Custom debug panelTailored to your appMust build and maintain it

FAQs

What does the devtools middleware do?
  • It connects your Zustand store to the Redux DevTools browser extension.
  • You can inspect state, track named actions, and use time-travel debugging.
How do you name actions in DevTools?
set(
  (s) => ({ count: s.count + 1 }),
  false,
  "increment"  // Action name shown in DevTools
);
  • Pass the action name as the third argument to set.
  • You can also pass an object: { type: "order/addItem", payload }.
What does the name option in the devtools config do?
  • It sets the store name shown in the Redux DevTools instance selector.
  • When you have multiple stores, each appears as a separate named instance.
How does time-travel debugging work with Zustand?
  • DevTools records each state change as a snapshot.
  • Jumping to a previous state replaces the entire store via setState.
  • All subscribed components re-render with the time-traveled state.
How do you disable devtools in production?
devtools(storeCreator, {
  name: "AppStore",
  enabled: process.env.NODE_ENV === "development",
});
Gotcha: Where should devtools go in the middleware stack?
  • Always outermost. If devtools is not the outermost middleware, it may not capture all state changes correctly.
  • Example: devtools(persist(immer((set) => ({ ... })))).
Gotcha: What happens if the third argument to set is used without devtools middleware?
  • It is silently ignored. The action name parameter only has effect when devtools middleware is active.
Does DevTools add runtime overhead?
  • Yes. It serializes state on every update to send to the extension.
  • Very large state trees can slow down DevTools. Use the serialize option to filter what is sent.
  • Always disable in production with enabled: false.
Can you enable stack traces for actions in DevTools?
devtools(storeCreator, {
  name: "Store",
  trace: true,
  traceLimit: 25,
});
  • The trace option adds a stack trace to each action in DevTools.
What is the TypeScript type for the action name parameter in set?
  • It is typed as string | { type: string; [key: string]: unknown }.
  • The devtools middleware does not change the store's type signature.
set(
  (state) => ({ count: state.count + 1 }),
  false,
  { type: "counter/increment", by: 1 }
);
Does the Redux DevTools browser extension need to be installed for the middleware to work?
  • Yes. Without the extension, the devtools middleware is a no-op (does nothing).
  • It does not cause errors if the extension is missing.