The biggest architectural mistake in frontend development is treating all application state the same. For years, teams defaulted to storing everything—from API response payloads to modal visibility flags—inside a single monolithic Redux store.
Modern React engineering splits state into two primary paradigms: Server State (asynchronous, external, shared) and Client State (synchronous, local, UI-driven).
┌─────────────────────────────────┐
│ React Application │
└────────────────┬────────────────┘
│
┌───────────────────────┴───────────────────────┐
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ Server State │ │ Client State │
│ (API Data, Cache, Sync) │ │ (Modals, Form UI, Theme) │
└────────────┬─────────────┘ └────────────┬─────────────┘
│ │
▼ ▼
[ TanStack Query ] [ Zustand / Redux ]
1. The Core State Distinction
Server State (Remote Data)
Ownership: Belongs entirely to the remote database and backend API.
Nature: Asynchronous, shared across multiple users, and prone to becoming stale.
Core Challenges: Caching, request deduplication, background polling, and cache invalidation.
Recommended Solution: TanStack Query (React Query).
Client State (Local UI Data)
Ownership: Belongs strictly to the individual user's browser session.
Nature: Synchronous, immediate, and isolated to local components or routes.
Core Challenges: Minimizing unnecessary re-renders and keeping state flow predictable.
Recommended Solution: Zustand (for lightweight setups) or Redux Toolkit (for strict enterprise workflows).
2. TanStack Query: Dedicated Server State Engine
TanStack Query manages fetching, caching, synchronization, and memory garbage collection for external API data. It eliminates boilerplate thunks, loading flags, and error reducers.
TypeScript
import { useQuery } from '@tanstack/react-query';
// Automatic caching, deduplication, and background refetching
export function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading, isError } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
staleTime: 1000 * 60 * 5, // 5 minutes fresh
});
if (isLoading) return <div>Loading user profile...</div>;
if (isError) return <div>Failed to load data.</div>;
return <h1>{user.name}</h1>;
}
Why it wins: Eliminates 70–80% of code previously placed in Redux. Features automatic retry, window-focus refetching, and declarative pagination out of the box.
3. Zustand: Minimalist Client State
Zustand offers a hook-based, zero-boilerplate global store. It weighs approximately 1.2 KB min+gzip, avoids context provider hell, and uses selective subscriptions to prevent unnecessary component re-renders.
TypeScript
import { create } from 'zustand';
interface UIState {
isSidebarOpen: boolean;
activeTheme: 'light' | 'dark';
toggleSidebar: () => void;
setTheme: (theme: 'light' | 'dark') => void;
}
export const useUIStore = create<UIState>((set) => ({
isSidebarOpen: false,
activeTheme: 'dark',
toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
setTheme: (theme) => set({ activeTheme: theme }),
}));
// Usage inside component (only re-renders if `isSidebarOpen` changes)
export function SidebarToggle() {
const isSidebarOpen = useUIStore((state) => state.isSidebarOpen);
const toggleSidebar = useUIStore((state) => state.toggleSidebar);
return <button onClick={toggleSidebar}>{isSidebarOpen ? 'Close' : 'Open'}</button>;
}
4. Redux Toolkit (RTK): The Enterprise Standard
Redux Toolkit remains the industry standard for large enterprise applications that require strict architectural guardrails, complex state pipelines, and time-travel debugging via Redux DevTools.
Best Use Cases: Applications requiring immutable audit trails (Fintech, Healthcare, complex ERP systems) or multi-team codebases needing unified standards.
Drawbacks: Larger bundle footprint (~14 KB min+gzip) and higher cognitive setup overhead compared to Zustand.
5. Architectural Decision Matrix
Is it data from a remote API / database?
├── YES ──► Use TanStack Query (React Query)
└── NO ──► Is it global client state needed across routes?
├── YES ──► Need strict audit trails & time-travel debugging?
│ ├── YES ──► Redux Toolkit (RTK)
│ └── NO ──► Zustand (Recommended Default)
└── NO ──► Local React `useState` / `useReducer`