Debugging React State with Zustand’s Logger Middleware: A Practical Guide
Learn how Zustand’s built‑in logger middleware can replace scattered console.logs, giving you a clear, chronological view of every state change in your React app. Setup, usage, and trade‑offs explained.
06 Feb 2026, 07:41 UTC

Problem: The Console Log Maze
In medium‑size React apps, the urge to sprinkle console.log statements everywhere to trace why a component re‑rendered can quickly become a maintenance nightmare. Logs are scattered, duplicated, and often hide the real cause of a state mutation. Zustand, a lightweight state‑management library, ships with a built‑in logger middleware that captures every state transition in a single, structured stream.
Thesis: One Middleware, Clear State History
Instead of scattering logs, add the logger middleware at store creation. It prints a concise line for each update: the action name, the previous state, and the new state. No extra code, no manual formatting, and no risk of forgetting to remove a log later.
1️⃣ Installation & Setup
First, install Zustand (v4.x or later) and its optional middleware package:
npm install zustand
# or
yarn add zustand
In your store file, import the middleware and wrap the store creation. The example below shows a guard that disables the logger in production, preventing performance hits and accidental state leaks.
import create from 'zustand';
import { devtools, logger } from 'zustand/middleware';
// Conditionally apply logger only in development
const useStore = create(
process.env.NODE_ENV !== 'production'
? devtools(
logger((set, get, api) => ({
// Store definition goes here
}))
)
: (set, get, api) => ({
// Store definition for production
})
);
Key points:
- Permissions: No special permissions required; runs in the browser.
- Placeholders: Replace the comment with your actual state logic.
- Risk: Logger adds a small runtime overhead; keep it out of production.
2️⃣ Using the Logger
Define a simple store, e.g., a todo list. Each action will automatically trigger a log line.
import create from 'zustand';
import { logger } from 'zustand/middleware';
const useTodoStore = create(
process.env.NODE_ENV !== 'production' ? logger((set) => ({
todos: [],
addTodo: (text) => set((state) => ({
todos: [...state.todos, { id: Date.now(), text }],
})),
removeTodo: (id) => set((state) => ({
todos: state.todos.filter((t) => t.id !== id),
})),
})) : (set) => ({
todos: [],
addTodo: (text) => set((state) => ({
todos: [...state.todos, { id: Date.now(), text }],
})),
removeTodo: (id) => set((state) => ({
todos: state.todos.filter((t) => t.id !== id),
})),
})
);
When addTodo is called, the console shows:
Zustand: action 'addTodo' from { todos: [] } to { todos: [{ id: 1697700000000, text: "Buy milk" }] }
Each log line contains three parts:
- Action Type: The function name that triggered the update.
- Previous State: A snapshot of the store before the change.
- Next State: The store after the change.
Verifying the Output
Open the browser console after performing an action. You should see an entry like the one above. If you build the app in production mode (e.g., vite build --mode production), the console should contain no logger output, confirming the guard worked.
3️⃣ Trade‑Offs & Limitations
| Aspect | Impact |
|---|---|
| Performance | Minor overhead due to JSON stringification of state snapshots. |
| Security | State can be exposed in the console; avoid sensitive data or strip it before logging. |
| Production Build Size | Logger code is retained unless conditionally excluded; always guard with process.env.NODE_ENV !== 'production'. |
| Non‑Serializable State | Functions or class instances will appear as [object Object] or cause errors. |
In practice, the benefits of having a single, chronological view of state changes outweigh the small performance cost in development. In production, simply omit the logger or replace it with a custom lightweight logger that masks sensitive fields.
4️⃣ Actionable Takeaway
- Wrap your Zustand store with
loggerduring development to replace scatteredconsole.logstatements. - Guard the middleware with
process.env.NODE_ENV !== 'production'or a custom flag to keep production clean. - Use the structured output to correlate UI actions with state mutations quickly—no more hunting for logs in the codebase.
- If you need to log in production, write a custom middleware that serializes only non‑sensitive fields.
With these steps, you’ll turn Zustand’s logger into a powerful debugging companion that scales with your application’s complexity.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.