GitHub

Client State with Zustand

Manage global UI state across multiple Zustand stores. Covers store slices, selectors, actions, and a computed filter store with reset.

01

Principle

Client state — UI toggles, filter state, user preferences — belongs in Zustand, not React Query. Each store owns one domain. Components read a slice of state via selectors and call actions. No prop drilling, no context boilerplate.

lightbulb

One store per feature domain. Never put server state (API data) in Zustand — if it comes from an endpoint, it belongs in React Query.

02

Rules

  • check_circle
    One store per domainuseAppStore for app-wide settings, useFilterStore for filters. Never mix concerns in a single store.
  • check_circle
    Actions inside the storeMutations happen in store actions, not in component event handlers. Keeps logic close to state.
  • check_circle
    Selectors over full-statePass selector functions: useAppStore(s => s.theme) not useAppStore(). Prevents unnecessary re-renders.
  • check_circle
    Reset is first-classAlways define a reset() action for stores that can be cleared. Useful for logout, navigation, and testing.
03

Pattern

stores/useFilterStore.ts
import { create } from 'zustand';
import type { UserRole, UserStatus } from '@/types';

interface FilterState {
  search: string;
  role: UserRole | null;
  status: UserStatus | null;
  setSearch: (search: string) => void;
  setRole: (role: UserRole | null) => void;
  setStatus: (status: UserStatus | null) => void;
  reset: () => void;
}

const initialState = { search: '', role: null, status: null };

export const useFilterStore = create<FilterState>((set) => ({
  ...initialState,
  setSearch: (search) => set({ search }),
  setRole: (role) => set({ role }),
  setStatus: (status) => set({ status }),
  reset: () => set(initialState),
}));

// Computed selector — avoids inline logic in components
export const useHasActiveFilters = () =>
  useFilterStore((s) => s.search !== '' || s.role !== null || s.status !== null);
04

Implementation

info

Version Compatibility

Requires React 19+ and the latest stable versions of all dependencies shown.

Zustand stores are client-side only. In Next.js, use them inside Client Components marked with 'use client'. No HydrationBoundary needed — client state is not serialized.

components/UserFilters.tsx
'use client';

import { useFilterStore, useHasActiveFilters } from '@/stores/useFilterStore';

export function UserFilters() {
  const { search, role, setSearch, setRole, reset } = useFilterStore();
  const hasFilters = useHasActiveFilters();

  return (
    <div className="flex gap-3">
      <input value={search} onChange={(e) => setSearch(e.target.value)}
        placeholder="Search..." />
      <select value={role ?? ''} onChange={(e) => setRole(e.target.value || null)}>
        <option value="">All roles</option>
        <option value="admin">Admin</option>
      </select>
      {hasFilters && <button onClick={reset}>Reset</button>}
    </div>
  );
}
05

Live Demo

App Store

Themedark

Filter Store

menu_book
React Patterns

Helping developers build robust React applications since 2025.

© 2025 React Patterns Cookbook. Built with ❤️ for the community.
react-principles