Skip to main content
We will be updating this guide soon based on our discussion in GitHub #2740.
Next.js is a popular server-side rendering framework for React that presents some unique challenges for using Zustand properly.

Challenges with Next.js

Keep in mind that Zustand store is a global variable (module state), which makes it optional to use Context. These challenges include:

Per-request Store

A Next.js server can handle multiple requests simultaneously. The store should be created per request and not shared across requests.

SSR Friendly

Next.js applications render twice—on the server and client. Different outputs cause hydration errors.

SPA Routing

Next.js supports hybrid client-side routing. To reset a store, initialize it at the component level using Context.

Server Caching

Recent Next.js versions (App Router) support aggressive server caching. Zustand’s module state is compatible with this.

Recommendations

Follow these guidelines for proper Zustand usage in Next.js:
  • No global stores - Don’t define stores as global variables. Create stores per request.
  • React Server Components should not read from or write to the store - RSCs can’t use hooks or context and shouldn’t interact with global stores.

Creating a Store Per Request

1

Configure TypeScript

Set up your tsconfig.json for Next.js:
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"]
}
Remove all comments from your tsconfig.json file.
2

Initialize the Store

Create a store factory function using zustand/vanilla:
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 })),
  }))
}
3

Create a Context Provider

Share the store using a context provider:
src/providers/counter-store-provider.tsx
'use client'

import { type ReactNode, createContext, useState, 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 [store] = useState(() => createCounterStore())
  return (
    <CounterStoreContext.Provider value={store}>
      {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)
}
This component is re-render-safe by checking the reference value, ensuring the store is only created once per request.

Usage with Different Architectures

Next.js offers two architectures: Pages Router and App Router. Usage is similar with slight differences.
1

Create a Page Component

src/components/pages/home-page.tsx
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>
  )
}
2

Wrap in _app.tsx

src/_app.tsx
import type { AppProps } from 'next/app'

import { CounterStoreProvider } from '@/providers/counter-store-provider'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <CounterStoreProvider>
      <Component {...pageProps} />
    </CounterStoreProvider>
  )
}
3

Use in Page

src/pages/index.tsx
import { HomePage } from '@/components/pages/home-page'

export default function Home() {
  return <HomePage />
}
For a store per route, create and share the store at the page component level:
src/pages/index.tsx
import { CounterStoreProvider } from '@/providers/counter-store-provider'
import { HomePage } from '@/components/pages/home-page'

export default function Home() {
  return (
    <CounterStoreProvider>
      <HomePage />
    </CounterStoreProvider>
  )
}
Only use this pattern if you need a separate store per route.

SSR and Hydration

Learn about server-side rendering and hydration concepts

TypeScript

Type-safe patterns for Zustand stores

Build docs developers (and LLMs) love