Back to React
React
easy
mid

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.

5 min read·~12 min to think through

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

KindTool
Server stateReact Query / SWR / RTK Query
Local UI stateuseState
Cross-tree config (theme, auth, feature flags)Context
Cross-tree app state (cart, editor)Zustand or Redux Toolkit
URL state (filters, page)Router
Form stateReact 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

tsx
const count = useCart((s) => s.items.length);   // bails out on unrelated changes

Not:

tsx
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

tsx
// 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

  • localStorage for 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 useReducer everywhere 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.

Senior engineer discussion

Seniors design slice ownership, push back on putting server state in client stores, and instrument state changes with devtools + breadcrumbs.

Related questions