Integración con Redux DevTools
Receta
Envuelve tu store con el middleware devtools para conectarte a la extensión de navegador Redux DevTools. Inspecciona el state, rastrea acciones y usa depuración con viaje en el tiempo.
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" }
)
);Ejemplo funcional
// 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 } // Acción con nombre y 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>Pedido ({status})</h2>
<button
onClick={() => addItem({ id: "p1", name: "Widget", price: 9.99 })}
>
Añadir 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)}>Eliminar</button>
</li>
))}
</ul>
<div>
<label>
Descuento %:
<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" ? "Enviando..." : "Enviar pedido"}
</button>
</div>
);
}Análisis profundo
Cómo funciona
- El middleware
devtoolsintercepta cada llamada asety envía el diff del state más un nombre de acción a la extensión Redux DevTools. - El tercer argumento de
set(state, replace, actionName)nombra la acción en DevTools. Sin él, las acciones aparecen como "anonymous". - Los nombres de acción pueden ser strings (
"increment") u objetos ({ type: "order/addItem", payload }) para una depuración más rica. - La opción
nameen la configuración de devtools establece el nombre del store que se muestra en el selector de instancias de DevTools. - La depuración con viaje en el tiempo te permite saltar a cualquier state anterior. Zustand reproduce los cambios de state mediante
setState. - La opción
enabledcontrola si DevTools se conecta. Establécela enfalseen producción para evitar sobrecarga.
Variaciones
Varios stores en DevTools:
// Cada store obtiene su propia instancia de DevTools
const useAuthStore = create<AuthState>()(
devtools((set) => ({ ... }), { name: "AuthStore" })
);
const useCartStore = create<CartState>()(
devtools((set) => ({ ... }), { name: "CartStore" })
);
// Ambos aparecen como instancias separadas en Redux DevToolsConexión a un servidor remoto de DevTools:
devtools(storeCreator, {
name: "RemoteStore",
// Conectar a una instancia remota de Redux DevTools
// Útil para depurar apps móviles o embebidas
});Opción trace para stack traces:
devtools(storeCreator, {
name: "Store",
trace: true, // Mostrar stack trace de cada acción
traceLimit: 25, // Máximo de frames del stack a mostrar
});Con otros middleware:
// devtools debe ser siempre el middleware más externo
const useStore = create<State>()(
devtools(
persist(
immer((set) => ({ ... })),
{ name: "persist-key" }
),
{ name: "DevToolsName" }
)
);Notas de TypeScript
- El middleware
devtoolsno cambia el tipo del store. Todos los tipos se conservan. - El parámetro de nombre de acción en
setestá tipado comostring | { type: string; [key: string]: unknown }.
// set completamente tipado con nombre de acción
set(
(state) => ({ count: state.count + 1 }), // Actualizador de state
false, // bandera replace
{ type: "counter/increment", by: 1 } // Acción para DevTools
);Trampas comunes
- Si
devtoolsno es el middleware más externo, DevTools puede no capturar correctamente todos los cambios de state. Envuelve siempre todo lo demás dentro dedevtools. - El tercer argumento de
set(nombre de acción) solo funciona cuando el middlewaredevtoolsestá activo. Sin él, el tercer argumento se ignora silenciosamente. - DevTools añade sobrecarga en tiempo de ejecución: serializa el state en cada actualización. Desactívalo con
enabled: falseen producción. - La depuración con viaje en el tiempo reemplaza todo el state. Si tu store tiene efectos secundarios en suscripciones, viajar en el tiempo puede dispararlos de forma inesperada.
- Los árboles de state muy grandes pueden ralentizar DevTools. Usa la opción
serializepara filtrar o limitar lo que se envía. - La extensión de navegador Redux DevTools debe estar instalada para que el middleware devtools tenga algún efecto. Sin ella, el middleware no hace nada.
Alternativas
| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Middleware de Redux DevTools | Viaje en el tiempo, inspección visual del state | Requiere extensión de navegador, sobrecarga en producción |
| Middleware de logging en consola | Sin dependencias, simple | Sin interfaz visual, sin viaje en el tiempo |
| React DevTools | Integrado, muestra el state del componente | No puede inspeccionar stores de Zustand directamente |
| Panel de depuración personalizado | Adaptado a tu app | Debes construirlo y mantenerlo |
Preguntas frecuentes
¿Qué hace el middleware devtools?
- Conecta tu store de Zustand a la extensión de navegador Redux DevTools.
- Puedes inspeccionar el state, rastrear acciones con nombre y usar depuración con viaje en el tiempo.
¿Cómo nombras acciones en DevTools?
set(
(s) => ({ count: s.count + 1 }),
false,
"increment" // Nombre de acción mostrado en DevTools
);- Pasa el nombre de la acción como tercer argumento de
set. - También puedes pasar un objeto:
{ type: "order/addItem", payload }.
¿Qué hace la opción name en la configuración de devtools?
- Establece el nombre del store que se muestra en el selector de instancias de Redux DevTools.
- Cuando tienes varios stores, cada uno aparece como una instancia nombrada separada.
¿Cómo funciona la depuración con viaje en el tiempo en Zustand?
- DevTools registra cada cambio de state como una instantánea.
- Saltar a un state anterior reemplaza todo el store mediante
setState. - Todos los componentes suscritos se re-renderizan con el state del viaje en el tiempo.
¿Cómo desactivas devtools en producción?
devtools(storeCreator, {
name: "AppStore",
enabled: process.env.NODE_ENV === "development",
});Trampa: ¿Dónde debe ir devtools en la pila de middleware?
- Siempre el más externo. Si
devtoolsno es el middleware más externo, puede no capturar correctamente todos los cambios de state. - Ejemplo:
devtools(persist(immer((set) => ({ ... })))).
Trampa: ¿Qué ocurre si se usa el tercer argumento de set sin el middleware devtools?
- Se ignora silenciosamente. El parámetro de nombre de acción solo tiene efecto cuando el middleware
devtoolsestá activo.
¿DevTools añade sobrecarga en tiempo de ejecución?
- Sí. Serializa el state en cada actualización para enviarlo a la extensión.
- Los árboles de state muy grandes pueden ralentizar DevTools. Usa la opción
serializepara filtrar lo que se envía. - Desactívalo siempre en producción con
enabled: false.
¿Puedes habilitar stack traces para acciones en DevTools?
devtools(storeCreator, {
name: "Store",
trace: true,
traceLimit: 25,
});- La opción
traceañade un stack trace a cada acción en DevTools.
¿Cuál es el tipo de TypeScript del parámetro de nombre de acción en set?
- Está tipado como
string | { type: string; [key: string]: unknown }. - El middleware
devtoolsno cambia la firma de tipos del store.
set(
(state) => ({ count: state.count + 1 }),
false,
{ type: "counter/increment", by: 1 }
);¿Es necesario instalar la extensión de navegador Redux DevTools para que el middleware funcione?
- Sí. Sin la extensión, el middleware devtools no hace nada.
- No provoca errores si falta la extensión.