GitHub

State Taxonomy

Three categories of state — local, shared, and server — and exactly which tool handles each one.

01

Principle

Not all state is the same. Before reaching for any state management library, ask one question: where does this data come from? Local state lives inside one component. Shared state is UI state needed by multiple components. Server state comes from an API and has its own lifecycle — loading, error, stale, and needs refreshing. Each category has a different tool, and mixing them up causes bugs that are hard to trace.

lightbulb

When you find yourself putting API data into Zustand, stop. Server state belongs in React Query. When you find yourself using React Query for a toggle or a modal, stop. UI state belongs in useState or Zustand.

02

Rules

  • check_circle
    Local state: useStateIf only one component needs it, keep it local. A form input value, a toggle, a hover state — these are all local state.
  • check_circle
    Shared state: ZustandIf multiple components need the same UI state — sidebar open/closed, active theme, search dialog open — use Zustand. This is not server data.
  • check_circle
    Server state: React QueryIf it comes from an API, it is server state. React Query handles caching, background refetching, loading states, and error states automatically.
  • check_circle
    Never put server state in ZustandStoring API data in Zustand means you manage caching, staleness, and loading manually. React Query already does this — use the right tool.
03

Pattern

The three categories — decision guide
// ─── LOCAL STATE ──────────────────────────────────────────────
// One component needs it. No sharing needed.
const [isOpen, setIsOpen] = useState(false);
const [inputValue, setInputValue] = useState('');
const [hovering, setHovering] = useState(false);

// ─── SHARED STATE (Zustand) ───────────────────────────────────
// Multiple components need the same UI state.
// This is NOT data from an API.
const { sidebarOpen, toggleSidebar } = useAppStore();
const { theme, setTheme } = useAppStore();
const { open: searchOpen } = useSearchStore();

// ─── SERVER STATE (React Query) ───────────────────────────────
// Comes from an API. Has loading, error, and cache lifecycle.
const { data: users, isLoading, error } = useUsers();
const { data: user } = useUser(id);

// ❌ WRONG — API data in Zustand
const useUserStore = create((set) => ({
  users: [],
  fetchUsers: async () => {
    const data = await usersService.getAll(); // ← belongs in React Query
    set({ users: data });
  },
}));

// ✅ RIGHT — API data in React Query, UI state in Zustand
const { data: users } = useUsers();             // React Query
const { activeFilter } = useFilterStore();      // Zustand
04

Implementation

info

Version Compatibility

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

In Next.js App Router, all three state categories are demonstrated in the starter template. See: github.com/sindev08/react-principles-nextjs → src/shared/stores/ and src/features/users/hooks/

State taxonomy — from react-principles-nextjs starter
// ─── SHARED STATE (Zustand) — src/shared/stores/ ─────────────
// useAppStore: theme + sidebar (app-wide UI)
'use client';
export const useAppStore = create<AppState>((set) => ({
  theme: 'light',
  sidebarOpen: true,
  toggleTheme: () => set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' })),
  toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
}));

// useFilterStore: search + role + status filters with reset
// useSearchStore: search dialog open/closed

// ─── SERVER STATE (React Query) — src/features/users/hooks/ ──
// useUsers: paginated user list from DummyJSON
'use client';
export function useUsers(params?: { limit?: number; skip?: number }) {
  return useQuery({
    queryKey: queryKeys.users.list(params ?? {}),
    queryFn: () => usersService.getAll(params),  // service → hook → component
  });
}
// useUser(id): single user detail via usersService.getById
// useCreateUser: mutation via usersService.create + cache invalidation
open_in_new

View stores in starter

View the real implementation in react-principles-nextjs

arrow_forward
menu_book
React Patterns

Helping developers build robust React applications since 2026.

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