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.

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.
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:
'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.
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:
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 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:
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.
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:
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.
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:
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.