Topics in this subject
React 4 min read Updated 5 Aug 2026

State Management

Local vs lifted vs global state, when Context suffices, and Redux Toolkit, Zustand, Jotai/Recoil.

The state ladder#

Reach for the least powerful tool that works. Most apps need far less global state than teams assume — much of what looks "global" is actually server state (cache of remote data) that belongs in React Query/SWR, not a store.

Level Tool Use when
Local useState/useReducer State used by one component/subtree
Lifted Move state to closest common parent A few siblings share it
Context useContext + provider Low-frequency global-ish values (theme, auth, locale)
Store lib Redux Toolkit / Zustand / Jotai Large, frequently-updated, cross-cutting client state
Server cache React Query / SWR / RTK Query Any data fetched from a server
flowchart TD
  A["Need to share state?"] -->|No| B["useState / useReducer (local)"]
  A -->|Few siblings| C["Lift to common parent"]
  A -->|Many, low-frequency| D["Context"]
  A -->|Many, high-frequency| E["Store: Redux Toolkit / Zustand / Jotai"]
  A -->|It's remote data| F["React Query / SWR"]

When Context is enough vs a library#

Context is dependency injection, not a state manager. Any consumer re-renders when the provider value changes — so it's great for stable, rarely-changing values and poor for high-frequency updates.

⚠️ Putting a fast-changing value in one big context re-renders every consumer. Fixes: split into multiple contexts, memoize the value, or move to a store with selectors so components subscribe to slices.

🟢 Rule of thumb: Context for theme/auth/i18n; a store when you have complex updates, many subscribers, middleware, or need selector-based subscriptions.

Redux Toolkit (RTK)#

The official, modern Redux. Kills boilerplate: createSlice generates action creators + reducers, uses Immer so you "mutate" drafts safely, and configures the store with good defaults (thunk, devtools).

import { configureStore, createSlice } from "@reduxjs/toolkit";
import { Provider, useSelector, useDispatch } from "react-redux";

const counter = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment: (s) => { s.value += 1; },        // Immer: safe "mutation"
    addBy: (s, action) => { s.value += action.payload; },
  },
});
export const { increment, addBy } = counter.actions;

const store = configureStore({ reducer: { counter: counter.reducer } });

function Counter() {
  const value = useSelector((s) => s.counter.value);  // subscribe to a slice
  const dispatch = useDispatch();
  return <button onClick={() => dispatch(addBy(5))}>{value}</button>;
}

function Root() { return <Provider store={store}><Counter /></Provider>; }

🎯 RTK's createAsyncThunk handles async flows; RTK Query is its built-in data-fetching/caching layer (an alternative to React Query when you're already on Redux).

Zustand — minimal store#

A tiny hook-based store: no provider, no boilerplate. The hook is the store; pass a selector to subscribe to just what you need.

import { create } from "zustand";

const useBearStore = create((set) => ({
  bears: 0,
  addBear: () => set((s) => ({ bears: s.bears + 1 })),
  reset: () => set({ bears: 0 }),
}));

function Bears() {
  const bears = useBearStore((s) => s.bears);       // selector = targeted subscription
  const addBear = useBearStore((s) => s.addBear);
  return <button onClick={addBear}>{bears}</button>;
}

🟢 No <Provider> needed and selectors give you fine-grained re-renders out of the box — why Zustand is popular for medium apps.

Jotai & Recoil (atoms, brief)#

Atomic state: state is composed of small atoms; components subscribe to individual atoms and only re-render when those change. Derived atoms compute from others (like Recoil selectors).

import { atom, useAtom } from "jotai";

const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2);   // derived

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  const [doubled] = useAtom(doubledAtom);
  return <button onClick={() => setCount((c) => c + 1)}>{count} / {doubled}</button>;
}

⚠️ Recoil is largely unmaintained/archived — prefer Jotai for the atomic model in new code.

Comparison & when to pick#

Option Boilerplate Provider Selectors Best for
Context Low Yes No (whole value) Theme, auth, low-frequency globals
Redux Toolkit Medium Yes Yes Large apps, middleware, devtools, team conventions
Zustand Very low No Yes Medium apps wanting simplicity + fine-grained updates
Jotai Low No (optional) Atom-level Bottom-up atomic/derived state
React Query Low Yes Query keys Server state: caching, refetch, mutations

🎯 The senior answer: separate server state from client state. Cache remote data with React Query; keep only genuine client/UI state in Redux/Zustand/Context. Don't dump fetched data into Redux.

Interview Q&A#

Q1. When is Context not enough? When the value updates frequently or has many consumers — every consumer re-renders on any change. Stores provide selector-based subscriptions so components only re-render for the slice they use.

Q2. Why Redux Toolkit over classic Redux? It removes boilerplate (createSlice auto-generates actions/reducers), bundles Immer for safe "mutations", and ships sane defaults (thunk, devtools, immutability checks). Hand-written action types/switch reducers are legacy.

Q3. Redux vs React Query — competitors? No. Redux manages client state; React Query manages server state (caching, background refetch, staleness, mutations). Using React Query removes most of what people previously stored in Redux.

Q4. Zustand vs Redux Toolkit? Zustand is provider-less with near-zero boilerplate and built-in selectors — great for small/medium apps. RTK offers structured conventions, middleware, and rich devtools that scale better on large teams.

Q5. What is atomic state (Jotai/Recoil)? State split into small atoms; components subscribe to individual atoms and re-render only when those change. Derived atoms compute from others. It's a bottom-up alternative to a single central store.