React · State at Scale

State Management

“Which state library should I use?” starts with a better question: what kind of state is it? Classify it, and the right tool is usually obvious — often no library at all.

Intermediate 6 min Lesson 27 of 42Examples target React 19 with Vite.
Before you start:Context API
0/42 in React
On this page · 6 min · Intermediate

Learning objectives#

  • Classify state as local, shared, server, URL or form state
  • Choose the simplest tool for each kind
  • Compare Context, Redux Toolkit and Zustand
  • Know why server data deserves its own tool

Explanation#

Local UI state (is this dropdown open?) → useState in the component.

Shared client state (cart, theme, signed-in user) → lift state up or Context. When it grows large or changes often, use a store library: Redux Toolkit (structured, great DevTools, common in large teams) or Zustand (tiny, simple hooks).

Server state (products, orders) is a cache of data owned by the backend. TanStack Query or SWR handle fetching, caching, refetching and invalidation — do not copy it into Redux by hand.

URL state (page number, filters, selected tab) → search params, so it survives refresh and can be shared.

Form state → controlled inputs or a form library like React Hook Form.

Rule of thumb: start with useState and props; add tools only when a real pain appears.

Syntax#

Text Illustrative
Kind of state       Example                 Tool
local UI            modal open?             useState
shared client       cart, theme, user       Context / Zustand / Redux Toolkit
server cache        products, orders        TanStack Query / SWR
URL                 ?page=2&q=pen           useSearchParams
form                sign-up fields          controlled inputs / React Hook Form

Code example: A tiny global store with useSyncExternalStore#

JSXstore.jsx Runnable
import { useSyncExternalStore } from 'react';


// A minimal store — the idea behind Zustand and Redux, in 20 lines
function createStore(initialState) {
  let state = initialState;
  const listeners = new Set();
  return {
    getState: () => state,
    setState: update => {
      state = { ...state, ...(typeof update === 'function' ? update(state) : update) };
      listeners.forEach(l => l());
    },
    subscribe: listener => {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
  };
}


const cartStore = createStore({ items: [] });


function useCartStore(selector) {
  return useSyncExternalStore(cartStore.subscribe, () => selector(cartStore.getState()), () => selector(cartStore.getState()));
}


const addItem = name => cartStore.setState(s => ({ items: [...s.items, name] }));


function CartCount() {
  const count = useCartStore(s => s.items.length);       // re-renders only when the count changes
  return <p>Cart: {count}</p>;
}


function AddButtons() {
  return ['Pen', 'Bag'].map(name => <button key={name} onClick={() => addItem(name)}>Add {name}</button>);
}


export default function App() {
  return (
    <>
      <CartCount />
      <AddButtons />   {/* no provider, no props */}
    </>
  );
}
Expected output
“Cart: 0” and two buttons. Each click increases the count. CartCount and AddButtons share state through the store without props or a provider — this is the model Zustand and Redux build on (with Redux adding actions, reducers and DevTools).

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. createStore keeps state outside React and notifies subscribers on change.
  2. useSyncExternalStore is React’s official hook for reading external stores safely.
  3. The selector lets CartCount subscribe to just the count.
  4. addItem can be called from anywhere — even outside components.

Real-world use case#

A large SaaS dashboard uses TanStack Query for all API data, Zustand for UI preferences and the sidebar state, search params for table filters, and plain useState inside each widget.

Common mistakes#

Putting server data in Redux by hand

You then reinvent caching, loading flags and refetching. Use a server-state library.

Global state for everything

A form’s input values in a global store re-render half the app on each keystroke. Keep it local.

Adding Redux on day one

Start simple. Many apps never need more than useState, context and a data-fetching library.

Best practices#

  • Classify state before choosing a tool
  • Keep state as local as possible
  • Use a server-cache library for API data
  • Put shareable UI state in the URL

Remember this#

Local → useState. Shared → Context/Zustand/Redux. Server → TanStack Query.

URL state = filters and pages that should survive refresh.

Start simple; add a library when it hurts.

Practice exercise#

Your task: Classify each: (1) whether a tooltip is visible, (2) the logged-in user, (3) the list of products from the API, (4) the current page number of a search, (5) the text in a comment box.

Need a hint?
Think: who owns it, who needs it, should it survive refresh?
Show solution
Text Illustrative
1. Tooltip visible       → local UI state (useState)
2. Logged-in user        → shared client state (Context or a store)
3. Products from the API → server state (TanStack Query / SWR, or fetch + state)
4. Search page number    → URL state (useSearchParams)
5. Comment box text      → form/local state (controlled input)

Mini quiz#

State Management quizMultiple choice Question 1 of 3

Which tool is designed for caching and refetching API data?

Score so far: 0/0

Want more? Practise all React MCQs · React interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.