Skip to the solution
MediumFrontend6 min read

How do you manage global state in a large React application?

539 views

The problem

Question presented to candidate: "How would you approach state management for a large React app? Walk me through the decision."

What a strong answer should cover:

  • The reframe that answers the question: separate server state from client state. Most of what people put in a global store is cached server data β€” it needs fetching, deduplication, invalidation and staleness rules, not storage.
  • Once server state is handled by a query library, the genuinely global client state left over is usually small: theme, auth session, a cart, UI preferences.
  • The ladder: local state β†’ lifted state β†’ composition β†’ Context β†’ a store, adopting each only when the previous one stops working.
  • Context is transport, not a store β€” no selectors, so every consumer re-renders.
  • What a store adds that Context cannot: selector-based subscriptions, so a component re-renders only for the slice it reads.
  • The realistic options and what distinguishes them: Redux Toolkit (conventions, devtools, large teams), Zustand (minimal, hook-based), Jotai (atomic, bottom-up), and useSyncExternalStore as the primitive they are all built on.
  • URL state is the forgotten category β€” filters, tabs, and pagination usually belong in the query string, where they are shareable and survive a refresh.
  • Anti-pattern: one giant store holding everything, which makes change frequency the maximum of everything in it.

Clarifying questions expected:

  • "How much of this is server data versus genuinely client-only state?"
  • "How large is the team β€” do we need enforced conventions and devtools?"
  • "Does any of it belong in the URL?"

Code / implementation expected: Optional. A small store with a selector demonstrates the point that Context cannot.

  • state management
  • context api
  • redux
  • global state

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactA 20-line store with selectors, next to the Context version
import { useSyncExternalStore, createContext, useContext, useState, useMemo, memo } from "react";

// ── A minimal store. This is roughly what Zustand is, minus the ergonomics.
//    The important part is selector-based subscription. ─────────────────────
function createStore(initial) {
  let state = initial;
  const listeners = new Set();
  return {
    getState: () => state,
    setState: (patch) => {
      state = { ...state, ...patch };
      listeners.forEach((l) => l());
    },
    subscribe: (l) => { listeners.add(l); return () => listeners.delete(l); },
  };
}

const store = createStore({ user: "ada", theme: "dark" });

// The selector is what Context cannot do: this component re-renders ONLY when
// the slice it selected actually changes.
function useStore(selector) {
  return useSyncExternalStore(
    store.subscribe,
    () => selector(store.getState()),
    () => selector(store.getState()),
  );
}

const counts = { storeUser: 0, ctxUser: 0 };

const StoreUser = memo(function StoreUser() {
  counts.storeUser++;
  const user = useStore((s) => s.user);       // subscribes to user only
  return <Line label="store + selector" value={user} n={counts.storeUser} />;
});

// ── The Context equivalent, for contrast. ─────────────────────────────────
const Ctx = createContext(null);

const CtxUser = memo(function CtxUser() {
  counts.ctxUser++;
  const { user } = useContext(Ctx);           // no way to subscribe to just user
  return <Line label="context" value={user} n={counts.ctxUser} />;
});

function Line({ label, value, n }) {
  return (
    <p style={{ margin: "4px 0", fontSize: 14 }}>
      <code style={{ display: "inline-block", minWidth: 150 }}>{label}</code>
      user: <strong>{value}</strong> Β· renders: <strong>{n}</strong>
    </p>
  );
}

export default function App() {
  const [theme, setTheme] = useState("dark");
  const ctxValue = useMemo(() => ({ user: "ada", theme }), [theme]);

  const changeTheme = () => {
    const next = theme === "dark" ? "light" : "dark";
    setTheme(next);
    store.setState({ theme: next });           // change ONLY the theme slice
  };

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 460 }}>
      <StoreUser />
      <Ctx value={ctxValue}><CtxUser /></Ctx>

      <p><button onClick={changeTheme}>change the theme only (now {theme})</button></p>

      <p style={{ color: "#666", fontSize: 13 }}>
        Click repeatedly. Neither component reads the theme. The store-backed one
        stays at 1 render because its selector returned the same user each time;
        the Context one climbs, because Context has no selectors and notifies
        every consumer on any change.
      </p>
    </div>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 57 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextMedium

React 19 ref as prop & ref cleanup β€” the new cleanup return from ref callbacks

Continue