//
Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Define funciones async como acciones del store. Usa set para gestionar los estados de carga y error junto con la operación async. No se necesita middleware especial.
import { create } from "zustand";
interface User {
id: string;
name: string;
email: string;
}
interface UserStore {
users: User[];
isLoading: boolean;
error: string | null;
fetchUsers: () => Promise<void>;
}
export const useUserStore = create<UserStore>((set) => ({
users: [],
isLoading: false,
error: null,
fetchUsers: async () => {
set({ isLoading: true, error: null });
try {
const res = await fetch("/api/users");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const users = await res.json();
set({ users, isLoading: false });
} catch (err) {
set({ error: (err as Error).message, isLoading: false });
}
},
}));// stores/product-store.ts
import { create } from "zustand";
interface Product {
id: string;
name: string;
price: number;
stock: number;
}
interface ProductStore {
products: Product[];
selectedProduct: Product | null;
isLoading: boolean;
error: string | null;
fetchProducts: () => Promise<void>;
fetchProduct: (id: string) => Promise<void>;
createProduct: (input: Omit<Product, "id">) => Promise<Product>;
updateStock: (id: string, delta: number) => Promise<void>;
}
export const useProductStore = create<ProductStore>((set, get) => ({
products: [],
selectedProduct: null,
isLoading: false,
error: null,
fetchProducts: async () => {
set({ isLoading: true, error: null });
try {
const res = await fetch("/api/products");
const products = await res.json();
set({ products, isLoading: false });
} catch (err) {
set({ error: (err as Error).message, isLoading: false });
}
},
fetchProduct: async (id) => {
set({ isLoading: true, error: null });
try {
const res = await fetch(`/api/products/${id}`);
const product = await res.json();
set({ selectedProduct: product, isLoading: false });
} catch (err) {
set({ error: (err as Error).message, isLoading: false });
}
},
createProduct: async (input) => {
set({ isLoading: true, error: null });
try {
const res = await fetch("/api/products", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
});
const newProduct = await res.json();
set((state) => ({
products: [...state.products, newProduct],
isLoading: false,
}));
return newProduct;
} catch (err) {
set({ error: (err as Error).message, isLoading: false });
throw err;
}
},
updateStock: async (id, delta) => {
// Actualización optimista
const previousProducts = get().products;
set((state) => ({
products: state.products.map((p) =>
p.id === id ? { ...p, stock: p.stock + delta } : p
),
}));
try {
await fetch(`/api/products/${id}/stock`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ delta }),
});
} catch (err) {
// Revertir en caso de fallo
set({ products: previousProducts, error: (err as Error).message });
}
},
}));// components/product-list.tsx
"use client";
import { useEffect } from "react";
import { useProductStore } from "@/stores/product-store";
export function ProductList() {
const products = useProductStore((s) => s.products);
const isLoading = useProductStore((s) => s.isLoading);
const error = useProductStore((s) => s.error);
const fetchProducts = useProductStore((s) => s.fetchProducts);
useEffect(() => {
fetchProducts();
}, [fetchProducts]);
if (isLoading) return <div>Cargando productos...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{products.map((p) => (
<div key={p.id}>
<h3>{p.name}</h3>
<p>${p.price} - {p.stock} en stock</p>
</div>
))}
</div>
);
}set puede llamarse varias veces en una acción async: una para establecer la carga, otra para establecer los datos o el error.set provoca una actualización de state sincrónica. React 18+ agrupa los renders, por lo que llamadas secuenciales rápidas a set pueden agruparse.get() para capturar el state actual antes de la operación async y luego revierten en caso de fallo.Helper genérico para acciones async:
function asyncAction<T>(
set: any,
fn: () => Promise<T>,
onSuccess: (data: T) => Partial<any>
) {
set({ isLoading: true, error: null });
return fn()
.then((data) => {
set({ ...onSuccess(data), isLoading: false });
return data;
})
.catch((err) => {
set({ error: (err as Error).message, isLoading: false });
throw err;
});
}
// Uso
const useStore = create((set) => ({
items: [],
isLoading: false,
error: null,
fetchItems: () =>
asyncAction(set, () => fetch("/api/items").then((r) => r.json()), (items) => ({ items })),
}));Estados de carga por acción:
interface Store {
data: Record<string, unknown>;
loading: Record<string, boolean>;
setLoading: (key: string, value: boolean) => void;
}
const useStore = create<Store>((set) => ({
data: {},
loading: {},
setLoading: (key, value) =>
set((s) => ({ loading: { ...s.loading, [key]: value } })),
}));AbortController para cancelación:
const useStore = create((set) => {
let controller: AbortController | null = null;
return {
data: null,
fetchData: async () => {
controller?.abort();
controller = new AbortController();
try {
const res = await fetch("/api/data", { signal: controller.signal });
set({ data: await res.json() });
} catch (err) {
if ((err as Error).name !== "AbortError") {
set({ error: (err as Error).message });
}
}
},
};
});Promise<void> o Promise<T> y deben tiparse en consecuencia.(err as Error).message es habitual, ya que los bloques catch reciben unknown.interface AsyncState {
isLoading: boolean;
error: string | null;
}
interface WithAsync<T> extends AsyncState {
data: T | null;
fetch: () => Promise<void>;
}isLoading compartido entre varias acciones async puede entrar en conflicto. Si fetchProducts y fetchProduct usan el mismo isLoading, uno puede sobrescribir al otro.set siguen ejecutándose. Usa AbortControllers o ignora respuestas obsoletas.set dentro de un try/catch con código async no revierte automáticamente en caso de error. Debes gestionar la lógica de reversión manualmente.set sigue actualizando el store (sin error, pero la limpieza del efecto del componente antiguo ya no existe).| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Acciones async inline | Simple, sin middleware | Gestión manual de estados de carga y error |
| SWR o React Query para fetching | Caché, deduplicación y reintento automáticos | Separado del store de Zustand |
| Redux Toolkit createAsyncThunk | Ciclo de vida estructurado (pending, fulfilled, rejected) | Requiere el ecosistema Redux |
| Middleware async personalizado | Reutilizable entre acciones | Complejidad añadida |
async que llame a set en distintos puntos (carga, éxito, error).fetchUsers: async () => {
set({ isLoading: true, error: null });
try {
const res = await fetch("/api/users");
const users = await res.json();
set({ users, isLoading: false });
} catch (err) {
set({ error: (err as Error).message, isLoading: false });
}
},get() para capturar el state actual antes de la operación async.set.set con la captura del state.Promise<T>.await del resultado, p. ej., para navegar tras una creación exitosa.fetchProducts y fetchProduct establecen isLoading, uno puede sobrescribir el estado de carga del otro.loading: Record<string, boolean>) para evitarlo.AbortController para cancelar la solicitud en curso anterior.AbortError en el bloque catch e ignóralo.controller?.abort();
controller = new AbortController();
const res = await fetch(url, { signal: controller.signal });set sigue actualizando el store: los stores de Zustand viven fuera de React.interface UserStore {
users: User[];
isLoading: boolean;
error: string | null;
fetchUsers: () => Promise<void>;
createUser: (input: Omit<User, "id">) => Promise<User>;
}Promise<void> para acciones fire-and-forget y Promise<T> cuando devuelvas datos.catch reciben unknown en TypeScript.Error: (err as Error).message.set({ isLoading: true }) / try / catch / set({ isLoading: false }) en una función reutilizable.Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥