File: nextjs.md | Updated: 11/15/2025
[!NOTE] We will be updating this guide soon based on our discussion in https://github.com/pmndrs/zustand/discussions/2740.
Next.js is a popular server-side rendering framework for React that presents
some unique challenges for using Zustand properly.
Keep in mind that Zustand store is a global
variable (AKA module state) making it optional to use a Context.
These challenges include:
Context.We have these general recommendations for the appropriate use of Zustand:
Let's write our store factory function that will create a new store for each request.
// tsconfig.json
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Note: do not forget to remove all comments from your
tsconfig.jsonfile.
// src/stores/counter-store.ts
import { 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 })),
}))
}
Let's use the createCounterStore in our component and share it using a context provider.
// src/providers/counter-store-provider.tsx
'use client'
import { type ReactNode, createContext, useRef, useContext } from 'react'
import { useStore } from 'zustand'
import { type CounterStore, createCounterStore } from '@/stores/counter-store'
export type CounterStoreApi = ReturnType<typeof createCounterStore>
export const CounterStoreContext = createContext<CounterStoreApi | undefined>(
undefined,
)
export interface CounterStoreProviderProps {
children: ReactNode
}
export const CounterStoreProvider = ({
children,
}: CounterStoreProviderProps) => {
const storeRef = useRef<CounterStoreApi | null>(null)
if (storeRef.current === null) {
storeRef.current = createCounterStore()
}
return (
<CounterStoreContext.Provider value={storeRef.current}>
{children}
</CounterStoreContext.Provider>
)
}
export const useCounterStore = <T,>(
selector: (store: CounterStore) => T,
): T => {
const counterStoreContext = useContext(CounterStoreContext)
if (!counterStoreContext) {
throw new Error(`useCounterStore must be used within CounterStoreProvider`)
}
return useStore(counterStoreContext, selector)
}
Note: In this example, we ensure that this component is re-render-safe by checking the value of the reference, so that the store is only created once. This component will only be rendered once per request on the server, but might be re-rendered multiple times on the client if there are stateful client components located above this component in the tree, or if this component also contains other mutable state that causes a re-render.
// src/stores/counter-store.ts
import { createStore } from 'zustand/vanilla'
export type CounterState = {
count: number
}
export type CounterActions = {
decrementCount: () => void
incrementCount: () => void
}
export type CounterStore = CounterState & CounterActions
export const initCounterStore = (): CounterState => {
return { count: new Date().getFullYear() }
}
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 })),
}))
}
// src/providers/counter-store-provider.tsx
'use client'
import { type ReactNode, createContext, useRef, useContext } from 'react'
import { useStore } from 'zustand'
import {
type CounterStore,
createCounterStore,
initCounterStore,
} from '@/stores/counter-store'
export type CounterStoreApi = ReturnType<typeof createCounterStore>
export const CounterStoreContext = createContext<CounterStoreApi | undefined>(
undefined,
)
export interface CounterStoreProviderProps {
children: ReactNode
}
export const CounterStoreProvider = ({
children,
}: CounterStoreProviderProps) => {
const storeRef = useRef<CounterStoreApi | null>(null)
if (storeRef.current === null) {
storeRef.current = createCounterStore(initCounterStore())
}
return (
<CounterStoreContext.Provider value={storeRef.current}>
{children}
</CounterStoreContext.Provider>
)
}
export const useCounterStore = <T,>(
selector: (store: CounterStore) => T,
): T => {
const counterStoreContext = useContext(CounterStoreContext)
if (!counterStoreContext) {
throw new Error(`useCounterStore must be used within CounterStoreProvider`)
}
return useStore(counterStoreContext, selector)
}
There are two architectures for a Next.js application: the Pages Router and the App Router. The usage of Zustand on both architectures should be the same with slight differences related to each architecture.
// src/components/pages/home-page.tsx
import { useCounterStore } from '@/providers/counter-store-provider.ts'
export const HomePage = () => {
const { count, incrementCount, decrementCount } = useCounterStore(
(state) => state,
)
return (
<div>
Count: {count}
<hr />
<button type="button" onClick={incrementCount}>
Increment Count
</button>
<button type="button" onClick={decrementCount}>
Decrement Count
</button>
</div>
)
}
// src/_app.tsx
import type { AppProps } from 'next/app'
import { CounterStoreProvider } from '@/providers/counter-store-provider.tsx'
export default function App({ Component, pageProps }: AppProps) {
return (
<CounterStoreProvider>
<Component {...pageProps} />
</CounterStoreProvider>
)
}
// src/pages/index.tsx
import { HomePage } from '@/components/pages/home-page.tsx'
export default function Home() {
return <HomePage />
}
Note: creating a store per route would require creating and sharing the store at page (route) component level. Try not to use this if you do not need to create a store per route.
// src/pages/index.tsx
import { CounterStoreProvider } from '@/providers/counter-store-provider.tsx'
import { HomePage } from '@/components/pages/home-page.tsx'
export default function Home() {
return (
<CounterStoreProvider>
<HomePage />
</CounterStoreProvider>
)
}
// src/components/pages/home-page.tsx
'use client'
import { useCounterStore } from '@/providers/counter-store-provider'
export const HomePage = () => {
const { count, incrementCount, decrementCount } = useCounterStore(
(state) => state,
)
return (
<div>
Count: {count}
<hr />
<button type="button" onClick={incrementCount}>
Increment Count
</button>
<button type="button" onClick={decrementCount}>
Decrement Count
</button>
</div>
)
}
// src/app/layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import { CounterStoreProvider } from '@/providers/counter-store-provider'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body className={inter.className}>
<CounterStoreProvider>{children}</CounterStoreProvider>
</body>
</html>
)
}
// src/app/page.tsx
import { HomePage } from '@/components/pages/home-page'
export default function Home() {
return <HomePage />
}
Note: creating a store per route would require creating and sharing the store at page (route) component level. Try not to use this if you do not need to create a store per route.
// src/app/page.tsx
import { CounterStoreProvider } from '@/providers/counter-store-provider'
import { HomePage } from '@/components/pages/home-page'
export default function Home() {
return (
<CounterStoreProvider>
<HomePage />
</CounterStoreProvider>
)
}