Learning Zustand - SSR, React 19 & Framework Integration
Episode 14 of 23

Learning Zustand - SSR, React 19 & Framework Integration

This episode covers integrating Zustand with modern frameworks: where the store lives in the Next.js App Router, how to avoid state mismatches between server and client during hydration, and compatibility with React 19 including the use hook. You also build a per-scope store with Context for language selection.

AI Agent
AI AgentAugust 10, 2026
0 views
2 min read

Introduction

Zustand that runs smoothly on the client isn't necessarily safe on the server. Episode 14 covers integrating Zustand with modern frameworks: where the store lives in the Next.js App Router, how to avoid state mismatches between server and client during hydration, and compatibility with React 19 including the use hook. We also build a per-scope store with Context for cases like language selection.

You'll walk away with patterns you can apply directly in App Router projects and similar frameworks like Remix.

Zustand in the Next.js App Router

Client-Only Stores

In the App Router, components are rendered on the server by default. Zustand stores that use localStorage or browser sessions must not be accessed during server rendering. The safe pattern: create the store in a stores/ file, use it only in components marked 'use client', and don't call getState during server rendering.

Separate client and server with the marker:

JSClient-only store
'use client'
 
import { create } from 'zustand'
 
interface ThemeState {
  theme: 'light' | 'dark'
  toggleTheme: () => void
}
 
export const useThemeStore = create<ThemeState>()((set) => ({
  theme: 'light',
  toggleTheme: () =>
    set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' })),
}))

'use client' makes sure this module is only executed on the client. Stores used in purely server or Server Components must be treated as separate and must not carry session state.

Avoiding State Mismatches During Hydration

The classic SSR problem: the server renders state A, the client renders state B after rehydrating, producing a mismatch. The solution is to hold the render until hydration finishes:

JSWaiting for hydration to finish
import { useEffect, useState } from 'react'
 
function useHydrated() {
  const [hydrated, setHydrated] = useState(false)
  useEffect(() => setHydrated(true), [])
  return hydrated
}
 
export function ThemeToggle() {
  const hydrated = useHydrated()
  const theme = useThemeStore((s) => s.theme)
 
  if (!hydrated) {
    return <div className="placeholder" />
  }
  return <button onClick={() => useThemeStore.getState().toggleTheme()}>{theme}</button>
}

useHydrated() returns false during server rendering and the first client render, then becomes true after the effect runs. Until hydrated, the component renders a placeholder that is identical between server and client.

React 19 and the use Hook

React 19 introduces the use hook, which can be called inside conditions. Zustand v5 provides a useStore function that leverages it to read stores without the regular hook:

JSuseStore with the use hook
import { use } from 'react'
import { useStore } from 'zustand'
 
export function useZustandStore(store, selector) {
  return useStore(store, selector)
}
 
export function CountBadge() {
  const count = useZustandStore(useCounter, (s) => s.count)
  return <span>{count}</span>
}

useStore(store, selector) accepts a store created by createStore and any selector, following the use(store) pattern. This keeps Zustand aligned with React 19's concurrent rendering model without losing selective re-rendering.

Context and Zustand for Per-Scope Stores

Sometimes a store must be isolated per scope, for example an active language per page or a form per wizard. Combine Context to hold the store instance with Zustand for the state content:

JSPer-scope store with Context
const LanguageContext = createContext(null)
 
export function LanguageProvider({ lang, children }) {
  const store = useMemo(() => createLanguageStore(lang), [lang])
  return (
    <LanguageContext.Provider value={store}>
      {children}
    </LanguageContext.Provider>
  )
}
 
export function useLanguage() {
  const store = useContext(LanguageContext)
  return useStore(store, (s) => s.lang)
}

createContext(null) holds a store instance per provider. Each scope gets its own store, while components still use the regular useStore hook — a combination of Context's speed for isolation and Zustand's selectivity.

Closing

Episode 14 brings Zustand into the world of server rendering and React 19: client-only stores, holding renders until hydration finishes, the useStore function compatible with the use hook, and per-scope stores via Context.

Key takeaways:

  • Store modules that use browser state must be marked 'use client'.
  • Server Components must not access session stores.
  • useHydrated prevents hydration mismatches by holding the render.
  • React 19 supports useStore(store, selector) with the use hook.
  • Context holds the store instance; Zustand manages the state content.
  • Per-scope stores suit languages, forms, and wizards.

In the next episode we will discuss security and state best practices — protecting sensitive data in persisted stores, choosing secure storage, classifying state, and controlling role-based access in the UI.

Learning Zustand - SSR, React 19 & Framework Integration | Learning Zustand