Skip to main content
Juliano Alves
Back to blog

Zustand for client state (and hydrating from the server)

3 min read
By Juliano Alves

Zustand offers a minimal API for client-side global state: no providers required, good TypeScript inference with slices, and middleware for logging, devtools, or persistence.

Basic store with slices#

import { create } from 'zustand';

type UiState = {
  sidebarOpen: boolean;
  toggleSidebar: () => void;
};

export const useUiStore = create<UiState>((set) => ({
  sidebarOpen: true,
  toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
}));

Use selectors in components: const open = useUiStore((s) => s.sidebarOpen) to avoid unnecessary re-renders.

When not to use Zustand#

Server-fetched page data belongs in React Query or Server Components, not a global store duplicated from props. Stores excel for UI chrome and cross-route client state (modals, wizards, theme toggles).

Hydrating from the server#

Pass serialized initial state from a Server Component into a small client bootstrap:

'use client';
import { useRef } from 'react';
import { useUiStore } from './store';

export function HydrateUi({ initial }: { initial: Partial<UiState> }) {
  const once = useRef(false);
  if (!once.current) {
    useUiStore.setState(initial);
    once.current = true;
  }
  return null;
}

Guard against double-invocation in Strict Mode when mutating global singletons.

persist middleware#

LocalStorage persistence is convenient for drafts; remember:

  • SSR: default storage is undefined—use skipHydration patterns from docs.
  • Sensitive data should never be persisted in plain text.

Summary#

Zustand fits small, explicit client stores. Combine with server-first data loading and keep persistence scoped to non-sensitive UI preferences unless you add encryption and threat modeling.

© 2026 Juliano Alves. All rights reserved.