//
Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
No Next.js App Router, os stores do Zustand são singletons que persistem entre requisições no servidor. Crie instâncias de store por requisição usando createStore + Context para evitar compartilhamento de estado entre usuários.
// 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 must be used within 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 must be used within 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>Welcome, {user?.name ?? "Guest"}</h1>
<pre>{JSON.stringify(serverData, null, 2)}</pre>
</div>
);
}create() em nível de módulo vazaria estado entre usuários.createStore de zustand/vanilla cria uma instância de store sem hooks do React. Encapsulá-lo em Context + useRef garante uma instância por árvore React.useRef) e nunca o recria, mesmo entre re-renderizações.initialState para o Client Provider, que inicializa o store.useStore(store, selector) do Zustand conecta um store vanilla ao React com assinaturas baseadas em seletores.persist deve lidar com o ambiente do servidor onde localStorage é indefinido.Store singleton simples (use apenas se o vazamento de estado SSR for aceitável):
// Aceitável para estado verdadeiramente global, como feature flags que são
// iguais para todos os usuários
import { create } from "zustand";
export const useFeatureFlags = create<{ flags: Record<string, boolean> }>(() => ({
flags: {},
}));Persist seguro para hidratação com onRehydrateStorage:
createStore<State>()(
persist(storeCreator, {
name: "store",
onRehydrateStorage: () => {
return (state, error) => {
if (error) console.error("Hydration failed:", error);
else console.log("Hydrated:", state);
};
},
})
);Stores por rota:
// app/checkout/layout.tsx
import { CheckoutProvider } from "@/providers/checkout-provider";
export default function CheckoutLayout({ children }: { children: React.ReactNode }) {
return <CheckoutProvider>{children}</CheckoutProvider>;
}
// O store só existe dentro do segmento de rota de checkoutcreateStore em vez de create para stores vanilla no Next.js. Os tipos são diferentes.import { createStore, StoreApi } from "zustand/vanilla";
import { useStore } from "zustand";
type MyStore = StoreApi<MyState>;
// Contexto tipado
const StoreContext = createContext<MyStore | null>(null);
// Hook seletor tipado
function useMyStore<T>(selector: (state: MyState) => T): T {
const store = useContext(StoreContext);
if (!store) throw new Error("Missing provider");
return useStore(store, selector);
}create() em nível de módulo no Next.js App Router compartilha estado entre todas as requisições renderizadas no servidor. O Usuário A poderia ver os dados do Usuário B. Sempre use o padrão Context + createStore para estado específico do usuário.persist acessa localStorage na importação, o que gera um erro em ambientes de servidor. Proteja com typeof window !== "undefined" ou use um storage no-op no servidor.useRef para o store impede a recriação, mas também significa que as alterações em initialState após a primeira renderização são ignoradas. Se precisar reinicializar, use uma prop key no Provider.useAppStore) em Server Components. Server Components não podem usar hooks.onRehydrateStorage do middleware persist é executado após a renderização inicial. Componentes podem exibir brevemente o estado padrão.| Abordagem | Prós | Contras |
|---|---|---|
| Context + createStore | Seguro para SSR, isolamento por requisição | Mais boilerplate que singleton |
| create() em nível de módulo | Simples, sem necessidade de provider | Vaza estado em SSR |
| Redux Toolkit + next-redux-wrapper | Padrão SSR maduro | Configuração pesada e complexa |
| Jotai com Provider | Atômico, seguro para SSR com Provider | Paradigma diferente |
createStore para isolamento por requisição.createStore de zustand/vanilla para criar instâncias de store (não hooks do React).useRef).useStore(store, selector).initialState para o Client Provider.// Server Component
const user = await getServerUser();
return <AppProvider initialState={{ user }}>{children}</AppProvider>;useRef mantém o mesmo valor entre renderizações sem disparar re-renderizações.if (!storeRef.current).initialState após a primeira renderização são ignoradas.create() (não createStore) para este padrão mais simples.localStorage não está disponível no servidor.typeof window !== "undefined" ou forneça um storage no-op para o servidor.// app/checkout/layout.tsx
export default function CheckoutLayout({ children }) {
return <CheckoutProvider>{children}</CheckoutProvider>;
}"use client".create retorna um hook React (UseBoundStore<StoreApi<State>>).createStore retorna um store vanilla (StoreApi<State>) sem bindings do React.createStore + useStore(store, selector) para o padrão de provider do 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("Missing provider");
return useStore(store, selector);
}T infere o tipo de retorno da função seletora.Revisado por Chris St. John·Última atualização: 7 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥