Master server state vs client state, when to use React Query / SWR vs Redux / Zustand, global vs local state patterns, and how to design scalable state architecture for complex applications — the complete system design answer for state management.
The most important insight in modern frontend state management is that there are two fundamentally different types of state: server state (data that lives on the server and you're synchronizing with) and client state (UI state that exists only on the frontend). Mixing them in the same store is the root cause of most state management complexity. Once you separate them, the right tool for each becomes obvious.
Data that originates on the server and your app displays. It's always potentially stale — another user might have changed it. It needs loading/error states, background refetching, caching, and pagination.
Best managed by: React Query, SWR, RTK Query
State that exists only in your frontend — the server doesn't know or care about it.
Best managed by: useState, useReducer, Zustand, or Context
React Query handles all the complexity of server state: caching, background refetching, deduplication, pagination, optimistic updates. You declare what you want; React Query handles when and how to fetch it.
function UserProfile({ userId }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
staleTime: 60_000, // treat as fresh for 1 min
refetchOnWindowFocus: true, // refetch when tab regains focus
});
// Mutations with optimistic updates
const mutation = useMutation({
mutationFn: updateUser,
onMutate: async (newUser) => {
await queryClient.cancelQueries(['user', userId]);
const previous = queryClient.getQueryData(['user', userId]);
queryClient.setQueryData(['user', userId], newUser); // optimistic
return { previous };
},
onError: (err, newUser, context) => {
queryClient.setQueryData(['user', userId], context.previous); // rollback
},
});
}
Redux is often added out of habit. Ask before reaching for it:
Redux genuinely shines for: complex client-side state with many actors and time-travel debugging needs (undo/redo, replaying actions), or large teams needing strict action-based patterns for auditability.
When you need shared client state without Redux's boilerplate. A Zustand store is a hook.
import { create } from 'zustand';
const useUIStore = create((set) => ({
sidebarOpen: false,
theme: 'light',
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
setTheme: (theme) => set({ theme }),
}));
// In any component — no Provider required
const { sidebarOpen, toggleSidebar } = useUIStore();
Is it from a server/API?
Yes → React Query / SWR / RTK Query
No →
Is it needed by more than one component tree?
No → useState / useReducer
Yes →
Is it simple? → Context (or Zustand)
Is it complex with many mutations? → Zustand (or Redux if team prefers)Redux is required for large React apps — many large production apps (including parts of Facebook) use React Query + local state with no global store. Redux is one option, not a requirement.
Context API is a state management solution — Context is a dependency injection mechanism, not a state manager. It doesn't handle caching, async, or performance optimization. Use it for stable, rarely-changing values.
You should store API responses in Redux — this is the root cause of 'why is Redux so complicated'. Server state (API data) should live in React Query, which handles caching and synchronization natively.
More state in global store = better — global state is harder to reason about, test, and colocate. Keep state as local as possible and only hoist when genuinely shared.
GitHub.com: uses React Query for PR/issue data, local state for UI (selected files in diff), and minimal global state for user session
Notion: document state is server-synced with real-time updates (similar to React Query mutations) + local state for selection, cursor position, panel open/closed
Linear: uses Zustand-like patterns for UI state + a custom sync layer for server state — the issue list is always synced from the server, not stored in Redux
What is the difference between server state and client state?
When would you choose Zustand over Redux Toolkit?
What is React Query's staleTime vs gcTime (cacheTime) and how do they interact?
What is optimistic updating and how do you implement it with React Query?
When should you use URL state instead of React state?
What are the performance pitfalls of React Context and how do you avoid them?
Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.