Skip to content
React rc state-management 4 min read

Choosing a State Solution

The hardest part of state management is not learning any single library—it is knowing which tool a given problem actually calls for. Reaching for Redux to toggle a dropdown is overkill; cramming a paginated server cache into Context is a recipe for stale data and re-render storms. The right answer almost always starts small and grows only when concrete pain appears. This page gives you a decision ladder to climb deliberately, from useState all the way to a global store, plus a hard rule about where server data belongs.

Start with the smallest scope that works

Most “state management” questions disappear once you ask where the state truly needs to live. The default is component-local state with useState or useReducer. Only when two or more siblings need the same value do you lift it to their nearest common parent.

import { useState } from "react";

function FilterBar() {
  const [query, setQuery] = useState("");
  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search…"
    />
  );
}

If lifting state turns a parent into a clearinghouse that passes props through three or four layers it never reads, you have hit prop drilling—the first real signal to consider Context.

Premature global state is the most common architecture mistake in React apps. Resist it. Local state is easier to delete, test, and reason about than anything stored in a shared store.

The decision ladder

Climb only as high as your problem forces you:

  1. Local / lifted state — UI that one component or a small subtree owns. No dependencies.
  2. Context API — low-frequency, app-wide values: theme, locale, the current user, feature flags.
  3. Zustand or Jotai — frequently-changing client state shared across distant parts of the tree, where Context would re-render too much.
  4. Redux Toolkit — large apps needing strict conventions, time-travel devtools, middleware, and a team that benefits from one enforced pattern.
  5. TanStack Queryany data that lives on a server. This is a separate axis, not a rung you graduate to.

Client state versus server state

The single most useful distinction is between client state (UI you own: open panels, form drafts, selected tabs) and server state (data that lives in a database and you merely cache). They have opposite needs: server state requires caching, deduping, refetching, and invalidation, while client state does not.

import { useQuery } from "@tanstack/react-query";

function UserProfile({ id }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ["user", id],
    queryFn: async () => {
      const res = await fetch(`/api/users/${id}`);
      if (!res.ok) throw new Error("Failed to load user");
      return res.json();
    },
  });

  if (isLoading) return <p>Loading…</p>;
  if (error) return <p>{error.message}</p>;
  return <h1>{data.name}</h1>;
}

Do not store server responses in Redux or Zustand by hand—you will reinvent caching badly. Let a query library own that, and keep your store for genuine client state.

Trade-off comparison

ToolBoilerplateDevtoolsScales toLearning curveBest for
useState / liftedNoneReact DevToolsOne subtreeTrivialLocal UI
Context APILowReact DevToolsSmall/mediumLowStable global values
ZustandVery lowYes (plugin)Medium/largeLowShared client state, minimal ceremony
JotaiLowYes (plugin)Medium/largeLow–mediumAtomic, derived state
Redux ToolkitMediumExcellentVery largeMediumBig teams, strict conventions
TanStack QueryLowExcellentAnyMediumServer state (separate concern)

Recommendations by app size

  • Prototype / small app: useState plus Context for theme and auth. Add TanStack Query the moment you fetch data.
  • Medium app: Zustand (or Jotai) for shared client state, TanStack Query for the server. This pairing covers the vast majority of products with almost no boilerplate.
  • Large app / large team: Redux Toolkit for client state when you need enforced patterns and rich devtools, RTK Query or TanStack Query for the server.

If you only remember one rule: put server data in a query library and keep your global store small. Most “we need Redux” instincts are really “we need a server cache.”

Best practices

  • Default to local state and lift only when a real shared need appears—never preemptively.
  • Treat server state as a cache, not as application state; reach for TanStack Query or RTK Query.
  • Reserve Context for low-frequency, stable values to avoid re-rendering large subtrees.
  • Pick Zustand or Jotai before Redux unless you specifically need Redux’s conventions and ecosystem.
  • Co-locate state with the code that uses it; distance between owner and consumer is a design smell.
  • Don’t mix concerns in one store—keep UI state and cached server data separate.
  • Choose the smallest tool that solves today’s problem; migrating up later is cheap, migrating down is not.
Last updated June 14, 2026
Was this helpful?