What are the best practices for managing state in large React applications?
Categorize state first: server (React Query), local UI (useState), cross-tree config (Context), cross-tree app state (Zustand/Redux with selectors), URL state (router), form state (RHF). Colocate where possible. Selectors > raw access. Feature-scoped slices with ownership in multi-team apps. Don't mirror server state in a client store. Avoid Context for hot state.
Cross-link to [[explain-your-strategy-for-managing-global-state-efficiently-in-a-react-app-with-]] for the multi-team angle. Best-practices summary here.
Categorize first
| Kind | Tool |
|---|---|
| Server state | React Query / SWR / RTK Query |
| Local UI state | useState |
| Cross-tree config (theme, auth, feature flags) | Context |
| Cross-tree app state (cart, editor) | Zustand or Redux Toolkit |
| URL state (filters, page) | Router |
| Form state | React Hook Form + Zod |
The #1 mistake: putting server state in Redux/Zustand and reinventing caching/revalidation.
Best practices
1. Colocate by default
State lives at the deepest component that uses it. Lifting prematurely = unnecessary re-renders fan out.
2. Selectors over raw state
const count = useCart((s) => s.items.length); // bails out on unrelated changesNot:
const { items } = useCart(); // re-renders on any state change
const count = items.length;3. Don't put hot state in Context
Context has no selector subscriptions — every consumer re-renders on any value change. Mouse position in Context = perf disaster. Move to Zustand or local state.
4. Derive, don't store
// BAD
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);
useEffect(() => setCount(items.length), [items]);
// GOOD
const [items, setItems] = useState([]);
const count = items.length;useMemo only for expensive derivations.
5. URL for shareable state
Filters, sort, page, search query — put them in the URL. Shareable, back-button safe, SSR-ready.
6. Slice ownership in multi-team apps
Feature-scoped slices (features/cart, features/checkout), each owned by a team via CODEOWNERS. Cross-slice access via exported selectors, not raw state.
7. SSR safety
Store created per request on the server — never share across requests.
8. Persistence with care
localStoragefor small prefs (theme, drafts).- IndexedDB for bigger / binary data.
- Wrap parse in try/catch; version the schema; migrate on read.
9. Optimistic UI for sends
Update store immediately; reconcile on response; rollback on failure.
10. Observability
- Redux DevTools (Zustand has a devtools middleware).
- Sentry breadcrumbs for actions.
- RUM for perf impact of state changes.
Anti-patterns
- Server state in client store.
- "One mega context" with everything.
- Lifting state to the root just in case.
- Storing derived values.
- Ad hoc
useReducereverywhere instead of a real store when scope grows. - Boolean useState explosion (
isOpen,isLoading,isError) instead of a status enum.
Interview framing
"Categorize state first: server → React Query, local UI → useState colocated, cross-tree config → Context, cross-tree app state → Zustand or Redux Toolkit with selector subscriptions, URL state → router, forms → RHF + Zod. Selectors with bailout equality; never put hot state in Context. Derive in render, don't store + sync via effects. URL as state for shareable filters. In multi-team apps, slice ownership via CODEOWNERS and slice boundaries via exported selectors. SSR: store per request. Persistence with schema versioning + try/catch. Optimistic UI for sends. The biggest anti-pattern remains server state in a client store."
Follow-up questions
- •Walk through how you'd refactor a context-heavy app.
- •Why selectors over raw state?
- •How do you handle SSR with Zustand?
Common mistakes
- •Server state in client store.
- •Hot state in Context.
- •Stored derived values.
- •Boolean state explosion.
Performance considerations
- •Selectors with shallow / custom equality cut fan-out. Local state avoids global re-renders.
Edge cases
- •Cross-slice events.
- •Persistence + hydration mismatches.
- •Optimistic UI rollback.
Real-world examples
- •Linear's slice architecture, Excalidraw on Zustand, Notion's editor state.