//
Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
In Next.js App Router, Zustand stores are singletons that persist across requests on the server. Create per-request store instances using createStore + Context to avoid shared state between users.
// 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() store would leak state between users.createStore from zustand/vanilla creates a store instance without React hooks. Wrapping it in Context + useRef ensures one instance per React tree.useRef) and never recreates it, even across re-renders.initialState to the Client Provider, which seeds the store.useStore(store, selector) from Zustand connects a vanilla store to React with selector-based subscriptions.persist middleware's storage adapter must handle the server environment where localStorage is undefined.Simple singleton store (use only if SSR state leaking is acceptable):
// Acceptable for truly global state like feature flags that are
// the same for all users
import { create } from "zustand";
export const useFeatureFlags = create<{ flags: Record<string, boolean> }>(() => ({
flags: {},
}));Hydration-safe persist with onRehydrateStorage:
createStore<State>()(
persist(storeCreator, {
name: "store",
onRehydrateStorage: () => {
return (state, error) => {
if (error) console.error("Hydration failed:", error);
else console.log("Hydrated:", state);
};
},
})
);Per-route stores:
// app/checkout/layout.tsx
import { CheckoutProvider } from "@/providers/checkout-provider";
export default function CheckoutLayout({ children }: { children: React.ReactNode }) {
return <CheckoutProvider>{children}</CheckoutProvider>;
}
// Store only exists within the checkout route segmentcreateStore instead of create for vanilla stores in Next.js. The types differ.import { createStore, StoreApi } from "zustand/vanilla";
import { useStore } from "zustand";
type MyStore = StoreApi<MyState>;
// Typed context
const StoreContext = createContext<MyStore | null>(null);
// Typed selector hook
function useMyStore<T>(selector: (state: MyState) => T): T {
const store = useContext(StoreContext);
if (!store) throw new Error("Missing provider");
return useStore(store, selector);
}create() store in Next.js App Router shares state across all server-rendered requests. User A could see User B's data. Always use the Context + createStore pattern for user-specific state.persist middleware accesses localStorage on import, which throws in server environments. Guard with typeof window !== "undefined" or use a no-op storage on the server.useRef for the store prevents re-creation, but it also means initialState changes after the first render are ignored. If you need to reinitialize, use a key prop on the Provider.useAppStore) in Server Components. Server Components cannot use hooks.onRehydrateStorage callback runs after the initial render. Components may flash default state briefly.| Approach | Pros | Cons |
|---|---|---|
| Context + createStore | SSR-safe, per-request isolation | More boilerplate than singleton |
| Module-level create() | Simple, no provider needed | Leaks state in SSR |
| Redux Toolkit + next-redux-wrapper | Mature SSR pattern | Heavy, complex setup |
| Jotai with Provider | Atomic, SSR-safe with Provider | Different paradigm |
createStore pattern for per-request isolation.createStore from zustand/vanilla to create store instances (not React hooks).useRef).useStore(store, selector).initialState to the Client Provider.// Server Component
const user = await getServerUser();
return <AppProvider initialState={{ user }}>{children}</AppProvider>;useRef maintains the same value across renders without triggering re-renders.if (!storeRef.current) check.initialState after the first render are ignored.create() (not createStore) for this simpler pattern.localStorage is not available on the server.typeof window !== "undefined" or provide a no-op storage for the server.// app/checkout/layout.tsx
export default function CheckoutLayout({ children }) {
return <CheckoutProvider>{children}</CheckoutProvider>;
}"use client".create returns a React hook (UseBoundStore<StoreApi<State>>).createStore returns a vanilla store (StoreApi<State>) without React bindings.createStore + useStore(store, selector) for the Next.js provider pattern.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 infers the return type from the selector function.Reviewed by Chris St. John·Last updated Jul 7, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥