Skip to content
D
Documentation

Set up Zustand in Next.js

how-to
2 min readUpdated

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:

bash
npm 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.

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 })),
  }))
}

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.

tsx
import 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:

tsx
import { 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:

tsx
import { 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 inputTypeDefaultEffect
initStateCounterStatedefaultInitState ({ 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, and getState, see Use a vanilla store in React.

See also

Was this page helpful?