Create a vanilla store per provider instance, expose it to client components, and mount the provider in either Next.js router without sharing a module-level store across server requests.
When to use this
Use a provider-owned store when Next.js server rendering and client components need to share application state. A module-level store can be shared by simultaneous server requests, so create the store inside the provider instead.
Create the store and provider
Install zustand, which includes the vanilla entry point:
bashnpm install zustand
Create the vanilla store with createStore from zustand/vanilla. The factory accepts initial count data and keeps the increment and decrement actions with that state.
tsimport { createStore } from 'zustand/vanilla'
export type CounterState = {
count: number
}
export type CounterActions = {
decrementCount: () => void
incrementCount: () => void
}
export type CounterStore = CounterState & CounterActions
export const defaultInitState: CounterState = {
count: 0,
}
export const createCounterStore = (
initState: CounterState = defaultInitState,
) => {
return createStore<CounterStore>()((set) => ({
...initState,
decrementCount: () => set((state) => ({ count: state.count - 1 })),
incrementCount: () => set((state) => ({ count: state.count + 1 })),
}))
}
The provider creates its store with a lazy state initializer, so a re-render of that provider keeps the same store instance. Its context uses StoreApi to retain the store's state type. The custom hook reads the context and subscribes through useStore; it throws a clear error if a component renders outside the provider.
tsx'use client'
import { createContext, useContext, useState, type ReactNode } from 'react'
import { useStore } from 'zustand'
import type { StoreApi } from 'zustand/vanilla'
import { createCounterStore } from '../stores/counter-store'
type CounterStore = {
count: number
decrementCount: () => void
incrementCount: () => void
}
type CounterStoreApi = StoreApi<CounterStore>
const CounterStoreContext = createContext<CounterStoreApi | undefined>(undefined)
export function CounterStoreProvider({ children }: { children: ReactNode }) {
const [store] = useState(() => createCounterStore())
return (
<CounterStoreContext.Provider value={store}>
{children}
</CounterStoreContext.Provider>
)
}
export function useCounterStore<T>(selector: (state: CounterStore) => T): T {
const store = useContext(CounterStoreContext)
if (!store) {
throw new Error('useCounterStore must be used within CounterStoreProvider')
}
return useStore(store, selector)
}
Add a client component that selects the count and action it needs. It renders Count: 0 initially; clicking Increment Count updates the displayed count, and Decrement Count lowers it.
tsx'use client'
import { useCounterStore } from '../providers/counter-store-provider'
type CounterStore = {
count: number
decrementCount: () => void
incrementCount: () => void
}
export function Counter() {
const count = useCounterStore((state: CounterStore) => state.count)
const incrementCount = useCounterStore(
(state: CounterStore) => state.incrementCount,
)
const decrementCount = useCounterStore(
(state: CounterStore) => state.decrementCount,
)
return (
<div>
Count: {count}
<hr />
<button type="button" onClick={incrementCount}>
Increment Count
</button>
<button type="button" onClick={decrementCount}>
Decrement Count
</button>
</div>
)
}
Mount the provider
Choose the mounting point for your router. A provider around the whole application shares one store instance within that provider tree. Put it at a route instead when that route needs its own store instance; do not create a new provider per route unless route-scoped state is required.
App Router
Render the client provider from the server root layout. The root layout stays a server component; the counter itself is a client component.
tsximport type { ReactNode } from 'react'
import { CounterStoreProvider } from '../providers/counter-store-provider'
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<CounterStoreProvider>{children}</CounterStoreProvider>
</body>
</html>
)
}
Render the counter from the route page:
tsximport { Counter } from '../components/counter'
export default function Page() {
return <Counter />
}
The page displays the counter inside the provider, and client-side button clicks update its selected state.
Pages Router
For route-scoped state, wrap the page content in the provider:
tsximport { CounterStoreProvider } from '../providers/counter-store-provider'
import { Counter } from '../components/counter'
export default function Home() {
return (
<CounterStoreProvider>
<Counter />
</CounterStoreProvider>
)
}
The page shows the same initial count and buttons; each click updates the count through its provider-owned store. For an app-wide store, wrap the page component in the provider from the Pages Router's custom application component.
Initial data and request boundaries
The provider sample initializes its store with the factory's default { count: 0 }. For route-specific state, place the provider at the route boundary.
| Factory input | Type | Default | Effect |
|---|---|---|---|
initState | CounterState | defaultInitState ({ count: 0 }) | Supplies the starting count for the store instance. |
Pitfalls
- Do not define one global store for the server to reuse across requests. Construct the store inside the provider so each provider instance owns its store.
- Do not read or write the store from a React Server Component. Keep store access in client components.
- Initialize server and client rendering with the same data; different initial output can cause hydration errors. See Handle persisted-state hydration for the guide's hydration-specific task.
- For vanilla-store details on
set,get,setState, andgetState, see Use a vanilla store in React.
Was this page helpful?