Intermediate1 questionFull Guide

React State Management Patterns — Complete Interview Guide

Master React state management — when to use local state, when to lift state, when Context is the right tool, and when to reach for Zustand or Redux. Plus server state (React Query) and URL state.

The Mental Model

React state management is a spectrum, not a single decision. At one end is useState for state owned by a single component. Next comes lifting state up for siblings that share it. Then Context for ambient data many components read. Then external stores (Redux, Zustand) for global, frequently-changing state with selective subscriptions. At the far end is server state (React Query) for data that lives on the server. The rule: keep state as local as possible, and only promote it up the chain when you have a concrete reason to.

The Explanation

The Four Layers of React State

Before choosing a tool, identify which category the state belongs to:

  1. Local state — owned by one component, no other component needs it
  2. Shared state — two or more related components need the same value
  3. Global ambient state — many disconnected components read it; changes are infrequent
  4. Global dynamic state — many components read and write; changes are frequent

Layer 1: Local State — useState and useReducer

Use useState for simple values (a string, boolean, or number) and useReducer when the next state depends on the previous state or multiple values must change together:

// useState — simple toggle
const [isOpen, setIsOpen] = useState(false);

// useReducer — multiple related state transitions
const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0 });

dispatch({ type: 'ADD_ITEM', payload: product });
dispatch({ type: 'REMOVE_ITEM', payload: productId });

Rule: if state only matters to one component (a dropdown open/close, an active tab, a form field value), keep it local. Moving state out is premature complexity.

Layer 2: Shared State — Lifting Up

When two sibling components need to share state, lift it to their closest common ancestor. The ancestor owns the state and distributes it as props:

// Parent owns the state — both children are controlled
function SearchPage() {
  const [query, setQuery] = useState('');

  return (
    <>
      <SearchInput value={query} onChange={setQuery} />
      <SearchResults query={query} />
    </>
  );
}

This is the correct solution for most shared state situations — no library needed.

Layer 3: Global Ambient State — Context

Context is right for data that is:

  • Read by many disconnected components across the tree
  • Infrequently updated (changes don't happen on every user action)
  • Essentially configuration: current user, theme, locale, feature flags
const ThemeContext = React.createContext('light');

function App() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={theme}>
      <AppContent />
      <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </ThemeContext.Provider>
  );
}
Context's weakness: all consumers re-render when the value changes — even if only one field in a large context object changed. For frequently-updated state, this is a performance bottleneck.

Layer 4: Global Dynamic State — Zustand / Redux

External stores solve Context's re-render problem with selective subscriptions — a component subscribes to only the slice of state it needs:

// Zustand store — simple to write, selective subscriptions built in
import { create } from 'zustand';

const useCartStore = create((set) => ({
  items: [],
  total: 0,
  addItem: (product) => set((state) => ({
    items: [...state.items, product],
    total: state.total + product.price,
  })),
}));

// Component subscribes only to 'total' — doesn't re-render when items change
function CartBadge() {
  const total = useCartStore(state => state.total); // selector
  return <span>{total}</span>;
}

// Different component subscribes only to 'items'
function CartList() {
  const items = useCartStore(state => state.items);
  return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
}

Zustand vs Redux: Zustand has minimal boilerplate (no actions/reducers/selectors setup), integrates seamlessly with TypeScript, and is easier to learn. Redux Toolkit is more structured, has excellent DevTools with time-travel debugging, and suits large teams where the extra convention helps. For most new projects, Zustand is the right default.

Server State — React Query / TanStack Query

A major insight: most of what developers store in global state is actually server data — data fetched from an API that lives on the server. Managing this in Redux or useState means manually handling loading, error, caching, invalidation, and re-fetching. React Query (TanStack Query) handles all of this:

// Without React Query — manual boilerplate
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
  fetch('/api/users').then(r => r.json()).then(setUsers).catch(setError).finally(() => setLoading(false));
}, []);

// With React Query — caching, deduplication, background refetch, built in
const { data: users, isLoading, error } = useQuery({
  queryKey: ['users'],
  queryFn: () => fetch('/api/users').then(r => r.json()),
});

The query key is a cache key. If multiple components call useQuery({ queryKey: ['users'] }), only one request fires. The result is cached and shared automatically.

URL State — Underused but Powerful

Some state belongs in the URL — filters, search queries, pagination, selected tabs. URL state persists across page refreshes, is shareable via link, and is accessible via the back button:

// Storing filter state in URL query params (React Router)
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get('category') ?? 'all';

function handleFilter(value) {
  setSearchParams({ category: value }); // survives page refresh, shareable link
}

Choosing the Right Layer — Quick Reference

  • One component → useState / useReducer
  • Sibling components → lift state up
  • Many components, rarely changes → Context
  • Many components, frequently changes → Zustand / Redux
  • Data from an API → React Query
  • Should survive refresh / be shareable → URL state

Common Misconceptions

⚠️

Many developers reach for Redux or Context immediately when state needs to be 'shared' — before adding any library, try lifting state to the nearest common ancestor. It's the simplest solution and handles most sharing requirements without any added complexity.

⚠️

Many developers put all application state in Redux or Zustand — this includes server data (fetched from APIs), which is better managed by React Query. Server state has different characteristics: it needs caching, deduplication, background refresh, and invalidation — none of which Redux provides out of the box.

⚠️

Many developers treat Context and Redux/Zustand as equivalent alternatives — they solve different problems. Context has no selector system; all consumers re-render on any value change. Zustand/Redux have selective subscriptions; a component watching cart.total doesn't re-render when user.name changes. Context is injection, Redux/Zustand is state management.

⚠️

Many developers think useState is always simpler than useReducer for everything — for state with multiple related transitions (a shopping cart with add/remove/clear operations, a form wizard with next/back/submit) useReducer is cleaner. It centralises the transition logic and makes it testable in isolation.

⚠️

Many developers store loading and error states manually alongside server data in useState — React Query eliminates this entirely. isLoading, isError, data, and isFetching are all returned automatically, along with caching, background refetch, and cache invalidation. Most 'global state' in Redux is actually server state that belongs in React Query.

⚠️

Many developers think Zustand is a toy and Redux is for 'serious' apps — Redux Toolkit and Zustand are both production-ready and widely used. The choice is about tradeoffs: Redux adds structure (useful for large teams), Zustand is minimal (useful for moving fast). Choosing based on team size and convention needs, not on perceived seriousness.

Where You'll See This in Real Code

E-commerce cart: cart state lives in Zustand — multiple components (CartIcon, CartPage, CheckoutSummary) subscribe to different slices. Adding to cart only re-renders CartIcon (badge count) and not the entire page. Server cart state synced via React Query mutations.

User authentication: the current user object lives in a UserContext — it's read by dozens of components (nav, profile, permissions), rarely changes, and infrequent updates don't cause performance issues.

Data tables with filtering: filter state (search query, sort column, page number) lives in URL params — the filtered view is shareable via link, persists on page refresh, and works with the browser back button.

Dashboard with multiple data sources: React Query manages all server data — useQuery for user data, useQuery for analytics data, useQuery for notifications. Each query has its own cache key, loading state, and background refresh interval.

Form state in a multi-step wizard: useReducer manages the wizard state — each step dispatches actions that update step data and advance the wizard. The centralised reducer makes it easy to validate, navigate, and submit.

Real-time collaborative app: Zustand holds the local document state; WebSocket messages dispatch updates that merge into the Zustand store; React Query handles saving to the server with optimistic updates and rollback on failure.

Interview Cheat Sheet

  • Local (1 component): useState for simple values; useReducer for multiple related transitions
  • Shared (siblings): lift state to nearest common ancestor — no library needed
  • Global ambient (many readers, rare updates): Context — theme, user, locale, feature flags
  • Global dynamic (many readers+writers, frequent updates): Zustand or Redux Toolkit
  • Server data (API): React Query — handles caching, loading, error, deduplication, background refetch
  • URL state (shareable, persists on refresh): useSearchParams (React Router) or next/navigation
  • Context limitation: all consumers re-render on any value change — no selector system
  • Zustand selector: useStore(state => state.count) — component only re-renders when count changes
  • Rule: keep state as local as possible; promote up the chain only when you have a concrete reason
💡

How to Answer in an Interview

  • 1.Frame it as a spectrum, not a choice: 'I think of state management as layers. Before reaching for any library I ask: can this stay local (useState)? If siblings need it, I lift it. If many disconnected components need it rarely, Context. If it changes frequently across many components, Zustand. If it's server data, React Query.' This structured answer impresses senior interviewers.
  • 2.The Context vs Zustand distinction is the depth question: 'Context is dependency injection — it has no selector system, so every consumer re-renders when the value changes. Zustand has selective subscriptions — a component watching cart.total doesn't re-render when user.name changes. For frequently-updating global state, Context has a performance problem that Zustand solves.'
  • 3.Server state insight separates candidates: 'A lot of what ends up in Redux is actually server data — data fetched from an API with loading and error states. React Query is a better home for it: it handles caching, deduplication, background refresh, and stale data — things you'd have to implement yourself in Redux.'
  • 4.useState vs useReducer — give the when-to-switch answer: 'I use useState for simple values. I switch to useReducer when state transitions involve multiple fields that change together, or when the next state depends on the previous state in complex ways. The main benefit is that the reducer is a pure function that's easy to test independently.'
  • 5.URL state is often overlooked — mentioning it impresses: 'Some state belongs in the URL — search queries, filters, pagination, selected tabs. URL state persists across refreshes, is shareable by link, and works with the browser back button. I use useSearchParams for this instead of putting it in component state.'
  • 6.Close with your personal default: 'For new projects I default to useState/useReducer locally, React Query for server data, and Zustand if I genuinely need cross-component dynamic state. I add Context for truly ambient config (theme, user). I'd use Redux Toolkit on a large team that benefits from the structure — not as a default.'

Practice Questions

1 question
#01

What are the different state management options in React and when do you use each?

EasyState Management PRO💡 Local state → Context → Zustand/Jotai → Redux — choose based on scope and update frequency

Related Topics

useState Hook — Complete React Interview Guide
Beginner·4–8 Qs
React Rendering & Performance Interview Questions
Advanced·4–8 Qs
useReducer Hook — Complete React Interview Guide
Intermediate·4–8 Qs
React Context API — Complete Interview Guide
Intermediate·8–12 Qs
🎯

Can you answer these under pressure?

Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.

Practice Free →Try Output Quiz