//
Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
En Next.js App Router, los stores de Zustand son singletons que persisten entre solicitudes en el servidor. Crea instancias de store por solicitud usando createStore + Context para evitar state compartido entre usuarios.
// stores/counter-store.ts
import { createStore } from "zustand/vanilla";
export interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
}
export const createCounterStore = (initialCount = 0) =>
createStore<CounterState>((set) => ({
count: initialCount,
increment: () => set((s) => ({ count: s.count + 1 })),
decrement: () => set((s) => ({ count: s.count - 1 })),
}));
export type CounterStore = ReturnType<typeof createCounterStore>;// providers/counter-provider.tsx
"use client";
import { createContext, useContext, useRef, ReactNode } from "react";
import { useStore } from "zustand";
import { createCounterStore, CounterState, CounterStore } from "@/stores/counter-store";
const CounterContext = createContext<CounterStore | null>(null);
export function CounterProvider({
children,
initialCount,
}: {
children: ReactNode;
initialCount?: number;
}) {
const storeRef = useRef<CounterStore>();
if (!storeRef.current) {
storeRef.current = createCounterStore(initialCount);
}
return (
<CounterContext.Provider value={storeRef.current}>
{children}
</CounterContext.Provider>
);
}
export function useCounterStore<T>(selector: (state: CounterState) => T): T {
const store = useContext(CounterContext);
if (!store) throw new Error("useCounterStore debe usarse dentro de CounterProvider");
return useStore(store, selector);
}// stores/app-store.ts
import { createStore } from "zustand/vanilla";
import { persist, createJSONStorage } from "zustand/middleware";
export interface AppState {
user: { id: string; name: string; email: string } | null;
theme: "light" | "dark";
setUser: (user: AppState["user"]) => void;
setTheme: (theme: AppState["theme"]) => void;
logout: () => void;
}
export type AppStoreType = ReturnType<typeof createAppStore>;
export const createAppStore = (initState?: Partial<AppState>) =>
createStore<AppState>()(
persist(
(set) => ({
user: null,
theme: "light",
...initState,
setUser: (user) => set({ user }),
setTheme: (theme) => set({ theme }),
logout: () => set({ user: null }),
}),
{
name: "app-storage",
storage: createJSONStorage(() =>
typeof window !== "undefined"
? localStorage
: {
getItem: () => null,
setItem: () => {},
removeItem: () => {},
}
),
partialize: (state) => ({ theme: state.theme }),
}
)
);// providers/app-provider.tsx
"use client";
import { createContext, useContext, useRef, ReactNode } from "react";
import { useStore } from "zustand";
import { createAppStore, AppState, AppStoreType } from "@/stores/app-store";
const AppStoreContext = createContext<AppStoreType | null>(null);
interface AppProviderProps {
children: ReactNode;
initialState?: Partial<AppState>;
}
export function AppProvider({ children, initialState }: AppProviderProps) {
const storeRef = useRef<AppStoreType>();
if (!storeRef.current) {
storeRef.current = createAppStore(initialState);
}
return (
<AppStoreContext.Provider value={storeRef.current}>
{children}
</AppStoreContext.Provider>
);
}
export function useAppStore<T>(selector: (state: AppState) => T): T {
const store = useContext(AppStoreContext);
if (!store) throw new Error("useAppStore debe usarse dentro de AppProvider");
return useStore(store, selector);
}// app/layout.tsx (Server Component)
import { AppProvider } from "@/providers/app-provider";
import { cookies } from "next/headers";
async function getServerUser() {
const cookieStore = await cookies();
const token = cookieStore.get("auth_token")?.value;
if (!token) return null;
const res = await fetch("https://api.example.com/me", {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) return null;
return res.json();
}
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const user = await getServerUser();
return (
<html lang="en">
<body>
<AppProvider initialState={{ user }}>
{children}
</AppProvider>
</body>
</html>
);
}// app/dashboard/page.tsx (Server Component)
import { DashboardClient } from "./dashboard-client";
async function getDashboardData() {
const res = await fetch("https://api.example.com/dashboard", {
next: { revalidate: 60 },
});
return res.json();
}
export default async function DashboardPage() {
const data = await getDashboardData();
return <DashboardClient serverData={data} />;
}// app/dashboard/dashboard-client.tsx
"use client";
import { useAppStore } from "@/providers/app-provider";
export function DashboardClient({ serverData }: { serverData: any }) {
const user = useAppStore((s) => s.user);
const theme = useAppStore((s) => s.theme);
return (
<div className={theme === "dark" ? "bg-gray-900 text-white" : "bg-white"}>
<h1>¡Hola, {user?.name ?? "Invitado"}</h1>
<pre>{JSON.stringify(serverData, null, 2)}</pre>
</div>
);
}create() a nivel de módulo filtraría state entre usuarios.createStore de zustand/vanilla crea una instancia de store sin hooks de React. Envolverlo en Context + useRef garantiza una instancia por árbol de React.useRef) y nunca lo vuelve a crear, ni siquiera entre re-renderizados.initialState al Provider de Cliente, que inicializa el store.useStore(store, selector) de Zustand conecta un store vanilla a React con suscripciones basadas en selectores.persist debe manejar el entorno del servidor, donde localStorage no está definido.Store singleton simple (úsalo solo si es aceptable que el state se filtre en SSR):
// Aceptable para state verdaderamente global como feature flags que son
// iguales para todos los usuarios
import { create } from "zustand";
export const useFeatureFlags = create<{ flags: Record<string, boolean> }>(() => ({
flags: {},
}));Persist seguro para hidratación con onRehydrateStorage:
createStore<State>()(
persist(storeCreator, {
name: "store",
onRehydrateStorage: () => {
return (state, error) => {
if (error) console.error("Falló la hidratación:", error);
else console.log("Hidratado:", state);
};
},
})
);Stores por ruta:
// app/checkout/layout.tsx
import { CheckoutProvider } from "@/providers/checkout-provider";
export default function CheckoutLayout({ children }: { children: React.ReactNode }) {
return <CheckoutProvider>{children}</CheckoutProvider>;
}
// El store solo existe dentro del segmento de ruta checkoutcreateStore en lugar de create para stores vanilla en Next.js. Los tipos difieren.import { createStore, StoreApi } from "zustand/vanilla";
import { useStore } from "zustand";
type MyStore = StoreApi<MyState>;
// Context tipado
const StoreContext = createContext<MyStore | null>(null);
// Hook selector tipado
function useMyStore<T>(selector: (state: MyState) => T): T {
const store = useContext(StoreContext);
if (!store) throw new Error("Falta el provider");
return useStore(store, selector);
}create() a nivel de módulo en Next.js App Router comparte state entre todas las solicitudes renderizadas en el servidor. El usuario A podría ver los datos del usuario B. Usa siempre el patrón Context + createStore para state específico del usuario.persist accede a localStorage al importar, lo que lanza un error en entornos de servidor. Protégelo con typeof window !== "undefined" o usa un almacenamiento no-op en el servidor.useRef para el store evita la recreación, pero también significa que los cambios de initialState después del primer renderizado se ignoran. Si necesitas reinicializar, usa una prop key en el Provider.useAppStore) en Server Components. Los Server Components no pueden usar hooks.onRehydrateStorage del middleware persist se ejecuta después del renderizado inicial. Los componentes pueden mostrar brevemente el state por defecto.| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Context + createStore | Seguro en SSR, aislamiento por solicitud | Más boilerplate que un singleton |
| create() a nivel de módulo | Simple, sin provider | Filtra state en SSR |
| Redux Toolkit + next-redux-wrapper | Patrón SSR maduro | Pesado, configuración compleja |
| Jotai con Provider | Atómico, seguro en SSR con Provider | Paradigma distinto |
createStore para aislamiento por solicitud.createStore de zustand/vanilla para crear instancias de store (no hooks de React).useRef).useStore(store, selector).initialState al Provider de Cliente.// Server Component
const user = await getServerUser();
return <AppProvider initialState={{ user }}>{children}</AppProvider>;useRef mantiene el mismo valor entre renderizados sin provocar re-renderizados.if (!storeRef.current).initialState después del primer renderizado se ignoran.create() (no createStore) para este patrón más simple.localStorage no está disponible en el servidor.typeof window !== "undefined" o proporciona un almacenamiento no-op para el servidor.// app/checkout/layout.tsx
export default function CheckoutLayout({ children }) {
return <CheckoutProvider>{children}</CheckoutProvider>;
}"use client".create devuelve un hook de React (UseBoundStore<StoreApi<State>>).createStore devuelve un store vanilla (StoreApi<State>) sin enlaces de React.createStore + useStore(store, selector) para el patrón de provider en Next.js.import { createStore, StoreApi } from "zustand/vanilla";
type MyStore = StoreApi<MyState>;function useAppStore<T>(selector: (state: AppState) => T): T {
const store = useContext(AppStoreContext);
if (!store) throw new Error("Falta el provider");
return useStore(store, selector);
}T infiere el tipo de retorno a partir de la función selector.Revisado por Chris St. John·Última actualización: 7 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥