React Interview Prep
State Management

When You Need State Management

When You Need State Management

LinkedIn Hook

You add Redux to a project with five components. You create actions, reducers, a store, selectors, and dispatch calls — for a counter and a login form.

That is not engineering. That is ceremony.

React already gives you useState, useReducer, useContext, and useRef. For most applications, these four tools handle every state requirement without installing a single extra package. The problem is not that React lacks state management — it is that developers reach for external libraries before understanding when React's built-in tools stop being enough.

In interviews, state management questions are not about Redux syntax. They are about judgment. "When would you introduce a state management library?" "What is the difference between local state and global state?" "How do you decide between Context API and Zustand?" "At what point does prop drilling become a real problem versus a perceived one?"

These questions test whether you make architectural decisions based on actual constraints or hype-driven defaults.

In this lesson, I break down local vs global state, the five signs that you genuinely need external state management, why Context API is not a state manager, and a decision flowchart you can use in interviews to explain your reasoning clearly.

If you have ever been asked "why did you choose Redux for this project?" and your answer was "because everyone uses it" — this lesson will give you a better one.

Read the full lesson -> [link]

#React #JavaScript #InterviewPrep #Frontend #CodingInterview #StateManagement #Redux #Zustand #ContextAPI #100DaysOfCode


When You Need State Management thumbnail


What You'll Learn

  • The difference between local state, lifted state, and global state — and when each is appropriate
  • Why React's built-in tools (useState, useReducer, useContext) are enough for most applications
  • The five concrete signs that you genuinely need an external state management library
  • Why Context API is a dependency injection tool, not a state manager — and why this distinction matters
  • A decision flowchart for choosing the right state approach in interviews and real projects

The Concept — State Scope and Ownership

Analogy: The Office Building

Imagine a 10-floor office building where people share information.

Local state is a sticky note on your own desk. Only you see it. Only you change it. When you need to track which tab is active in a component, that is a sticky note — useState inside that one component. Nobody else needs to know.

Lifted state is a whiteboard in a shared meeting room. Two or three teams on the same floor need the same data — say, the currently selected project. Instead of each team keeping their own copy (which would get out of sync), you put one whiteboard in the shared room, and everyone reads from it. In React, this means moving state to the nearest common parent and passing it down via props.

Global state is the announcement board in the lobby. The building's operating hours, fire drill schedule, and CEO announcements live there. Every floor, every team, every person can read it. In React, this is state that many unrelated components across the tree need — the authenticated user, the theme, the locale, or the shopping cart.

The mistake most developers make: They put sticky-note information on the lobby announcement board. Your modal's open/closed state does not belong in Redux. Your form's input values do not belong in a global store. When everything is "global," you lose the ability to reason about which component owns which data — and debugging becomes archaeology.

The rule: State should live as close as possible to where it is used. Only promote it when multiple unrelated parts of the tree genuinely need it.


Local State vs Lifted State vs Global State

Code Example 1: Local State — Keep It Where It Belongs

import { useState } from "react";

function Accordion({ title, children }) {
  // This state is local — only this component cares whether it is open
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div className="accordion">
      <button onClick={() => setIsOpen(!isOpen)}>
        {title} {isOpen ? "▲" : "▼"}
      </button>
      {/* Only render children when the accordion is open */}
      {isOpen && <div className="accordion-body">{children}</div>}
    </div>
  );
}

function FAQ() {
  return (
    <div>
      <Accordion title="What is React?">
        <p>A JavaScript library for building user interfaces.</p>
      </Accordion>
      <Accordion title="What is JSX?">
        <p>A syntax extension that looks like HTML but compiles to JavaScript.</p>
      </Accordion>
    </div>
  );
}

// Each Accordion manages its own open/closed state independently.
// No parent needs to know. No global store needed.
// Output when clicking "What is React?":
// The accordion toggles open, showing the paragraph.
// The other accordion remains unchanged.

Key point: If only one component reads and writes a piece of state, it is local state. Putting it anywhere else adds complexity for zero benefit.


Code Example 2: Lifted State — Shared Between Siblings

import { useState } from "react";

// Parent owns the state because both children need it
function TemperatureConverter() {
  const [celsius, setCelsius] = useState(0);

  // Single source of truth — Fahrenheit is derived, not stored separately
  const fahrenheit = (celsius * 9) / 5 + 32;

  return (
    <div>
      <CelsiusInput value={celsius} onChange={setCelsius} />
      <FahrenheitDisplay value={fahrenheit} />
      <TemperatureVerdict celsius={celsius} />
    </div>
  );
}

function CelsiusInput({ value, onChange }) {
  return (
    <label>
      Celsius:
      <input
        type="number"
        value={value}
        // Convert string input to number before passing up
        onChange={(e) => onChange(Number(e.target.value))}
      />
    </label>
  );
}

function FahrenheitDisplay({ value }) {
  return <p>Fahrenheit: {value.toFixed(1)}</p>;
}

function TemperatureVerdict({ celsius }) {
  // Simple derived logic — no state needed here
  if (celsius >= 100) return <p>The water is boiling.</p>;
  if (celsius <= 0) return <p>The water is frozen.</p>;
  return <p>The water is liquid.</p>;
}

// Typing 100 in the Celsius input:
// Output: "Fahrenheit: 212.0"
// Output: "The water is boiling."

Key point: When two sibling components need the same data, lift state to their closest common parent. This is not prop drilling — it is intentional data flow. Prop drilling becomes a problem only when state must pass through many intermediate components that do not use it.

When You Need State Management visual 1


When React's Built-in Tools Are Enough

React gives you four tools for state management:

ToolPurposeScope
useStateSimple state for a single componentLocal
useReducerComplex state with multiple related values or transitionsLocal or lifted
useContextPass data through the tree without prop drillingAny level
useRefMutable value that does not trigger re-rendersLocal

For most applications — even production ones — this is everything you need. A CRUD app with authentication, a dashboard with filters, an e-commerce product page — these can all be built with useState, useReducer, and useContext alone.

Code Example 3: Context + useReducer Covers Most "Global" Needs

import { createContext, useContext, useReducer } from "react";

// Define the shape of our global state
const initialState = {
  user: null,
  theme: "light",
  notifications: [],
};

// Reducer handles all state transitions in one place
function appReducer(state, action) {
  switch (action.type) {
    case "LOGIN":
      return { ...state, user: action.payload };
    case "LOGOUT":
      return { ...state, user: null, notifications: [] };
    case "TOGGLE_THEME":
      return { ...state, theme: state.theme === "light" ? "dark" : "light" };
    case "ADD_NOTIFICATION":
      return { ...state, notifications: [...state.notifications, action.payload] };
    case "CLEAR_NOTIFICATIONS":
      return { ...state, notifications: [] };
    default:
      return state;
  }
}

// Create context for state and dispatch separately
const StateContext = createContext(null);
const DispatchContext = createContext(null);

function AppProvider({ children }) {
  const [state, dispatch] = useReducer(appReducer, initialState);

  // Splitting state and dispatch into separate contexts prevents
  // components that only dispatch from re-rendering when state changes
  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
}

// Custom hooks for clean access
function useAppState() {
  const context = useContext(StateContext);
  if (context === null) throw new Error("useAppState must be used within AppProvider");
  return context;
}

function useAppDispatch() {
  const context = useContext(DispatchContext);
  if (context === null) throw new Error("useAppDispatch must be used within AppProvider");
  return context;
}

// Components use the custom hooks — clean and testable
function Header() {
  const { user, theme } = useAppState();
  const dispatch = useAppDispatch();

  return (
    <header className={theme}>
      <span>{user ? `Hello, ${user.name}` : "Not logged in"}</span>
      <button onClick={() => dispatch({ type: "TOGGLE_THEME" })}>
        Switch to {theme === "light" ? "dark" : "light"}
      </button>
    </header>
  );
}

function LoginButton() {
  const dispatch = useAppDispatch();

  // This component only dispatches — it does not read state
  // Because dispatch context is separate, it will not re-render when state changes
  return (
    <button onClick={() => dispatch({ type: "LOGIN", payload: { name: "Alice" } })}>
      Log In
    </button>
  );
}

// Output after clicking "Log In":
// Header shows: "Hello, Alice"
// Output after clicking "Switch to dark":
// Header applies dark theme class, button text changes to "Switch to light"

Key point: This pattern — useReducer plus split contexts — handles authentication, theming, and notification state for an entire application. No npm install required. Most teams would reach for Redux or Zustand here, but the built-in tools are sufficient when the update frequency is low and the subscriber count is moderate.


The Five Signs You Need External State Management

Context + useReducer has limits. Here are the concrete signals that an external library (Redux Toolkit, Zustand, Jotai, etc.) would genuinely help:

Sign 1: Many components subscribe to frequently changing state

Context re-renders every subscriber when any part of the context value changes. If 30 components read from a context and the notification count updates every second, all 30 re-render every second — even if they only care about the theme.

Sign 2: You need fine-grained subscriptions

External libraries let components subscribe to specific slices of state. useSelector(state => state.cart.itemCount) in Redux or useStore(state => state.count) in Zustand will only re-render when that specific value changes. Context cannot do this without splitting into dozens of separate contexts.

Sign 3: You need middleware for side effects

If your state transitions trigger async operations (API calls, WebSocket messages, analytics events), external libraries provide middleware. Redux has thunks and sagas. Zustand has middleware. Context + useReducer forces you to handle side effects outside the state system, which becomes messy at scale.

Sign 4: You need time-travel debugging or state persistence

Redux DevTools let you inspect every action, rewind state, and replay transitions. Zustand and Jotai have persist middleware that saves state to localStorage automatically. These capabilities do not exist in Context.

Sign 5: Multiple unrelated data domains share complex relationships

When you have cart state, user state, product state, and UI state — each with their own actions, selectors, and derived data — a centralized store with proper tooling (selectors, normalized state, immer integration) prevents the spaghetti that emerges from five separate contexts with five separate reducers.

When You Need State Management visual 2


Code Example 4: When Context Breaks Down — The Re-render Problem

import { createContext, useContext, useState, memo } from "react";

// A single context holding everything — common mistake
const AppContext = createContext(null);

function AppProvider({ children }) {
  const [user, setUser] = useState({ name: "Alice" });
  const [theme, setTheme] = useState("light");
  const [counter, setCounter] = useState(0);

  // Every time ANY value changes, a new object is created
  // Every subscriber re-renders — even if they only use one value
  const value = { user, setUser, theme, setTheme, counter, setCounter };

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

// This component only needs the theme
const ThemeDisplay = memo(function ThemeDisplay() {
  const { theme } = useContext(AppContext);
  console.log("ThemeDisplay rendered");
  // This renders every time counter changes — even though it only uses theme
  // React.memo does NOT help here because useContext bypasses memo
  return <p>Current theme: {theme}</p>;
});

// This component updates the counter rapidly
function Counter() {
  const { counter, setCounter } = useContext(AppContext);
  return (
    <div>
      <p>Count: {counter}</p>
      <button onClick={() => setCounter((c) => c + 1)}>Increment</button>
    </div>
  );
}

// Clicking "Increment" 10 times:
// Output in console: "ThemeDisplay rendered" appears 10 times
// ThemeDisplay re-renders every time even though theme never changed.
// This is the core limitation of Context for high-frequency state.

This is the moment you need an external library. When Context causes performance problems that splitting into more contexts cannot reasonably solve, a library with fine-grained subscriptions is the right answer.


Common Mistakes

Mistake 1: Using global state for everything

// WRONG — form input state in a global store
const useFormStore = create((set) => ({
  email: "",
  password: "",
  setEmail: (email) => set({ email }),
  setPassword: (password) => set({ password }),
}));

// Every keystroke updates the global store
// Every subscriber re-renders on every keystroke
function LoginForm() {
  const { email, password, setEmail, setPassword } = useFormStore();
  return (
    <form>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input value={password} onChange={(e) => setPassword(e.target.value)} />
    </form>
  );
}

// RIGHT — form state is local, only the result goes global
function LoginForm() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const login = useAuthStore((state) => state.login);

  function handleSubmit(e) {
    e.preventDefault();
    // Only the final result touches global state
    login(email, password);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input value={password} onChange={(e) => setPassword(e.target.value)} />
      <button type="submit">Log In</button>
    </form>
  );
}

// Rule: Transient state (typing, hovering, dragging) should always be local.
// Only the committed result (submitted form, completed action) goes global.

Mistake 2: Confusing Context API with a state manager

// Context is a DELIVERY MECHANISM, not a state manager.
// It broadcasts a value to subscribers. That is all.

// People say "use Context instead of Redux" — but they solve different problems.
// Context = how you deliver state (replaces prop drilling)
// Redux/Zustand = how you manage state (actions, reducers, selectors, middleware)

// You can use Context to deliver Redux-like state (useReducer + Context),
// but Context itself does not give you:
// - Fine-grained subscriptions (every subscriber re-renders)
// - Middleware (no built-in way to intercept actions)
// - DevTools (no time-travel debugging)
// - Selectors (no derived state optimization)

// Context is to state management what a pipe is to water treatment.
// The pipe delivers water. It does not filter, heat, or purify it.

Mistake 3: Adding a state library on day one of a new project

// Day 1: "We might need global state later, so let's add Redux now."
// This leads to:
// - Boilerplate for simple features that only need useState
// - Developers putting local UI state in the store out of habit
// - Harder onboarding for new team members
// - Over-engineered code that is harder to test

// Better approach: Start with useState and useReducer.
// Add Context when prop drilling spans more than 3-4 levels.
// Add an external library only when you hit a specific pain point:
//   - Performance problems from Context re-renders
//   - Need for middleware, DevTools, or persistence
//   - Multiple complex data domains with shared relationships

// The best state management is the one you did not install.

Interview Questions

Q: What is the difference between local state and global state in React?

Local state is managed within a single component using useState or useReducer and is only accessible by that component. Examples include form inputs, toggles, and modal visibility. Global state is accessible to many components across the application, typically for data like the authenticated user, theme preferences, or a shopping cart. The key principle is that state should be kept as local as possible and only promoted to a broader scope when multiple unrelated components need it.

Q: When would you choose Context API over an external state management library?

Context API is ideal when you need to share data that changes infrequently with many components — such as theme, locale, or authenticated user information. It eliminates prop drilling without adding dependencies. However, Context re-renders all subscribers whenever the provided value changes, making it unsuitable for frequently updating state read by many components. If I need fine-grained subscriptions, middleware for async side effects, or DevTools for debugging, I would choose an external library like Zustand or Redux Toolkit.

Q: Why is Context API not a state manager?

Context is a dependency injection mechanism — it broadcasts a value to all subscribers in the tree. It does not provide state management features like fine-grained subscriptions (all consumers re-render when any part of the value changes), middleware (no way to intercept dispatched actions for side effects), selectors (no built-in derived state optimization), or DevTools (no time-travel debugging). You can combine Context with useReducer to create a basic state management pattern, but Context itself is the delivery pipe, not the water treatment plant.

Q: What are the signs that an application has outgrown React's built-in state tools?

Five concrete signs: (1) Many components subscribe to the same context and re-render excessively when unrelated data changes. (2) You need components to subscribe to specific slices of state rather than the entire context value. (3) State transitions trigger async operations that are becoming difficult to manage alongside the reducer. (4) You need time-travel debugging or state persistence to localStorage. (5) Multiple complex data domains with interrelated selectors and derived state make the codebase difficult to maintain with separate contexts.

Q: Walk me through your decision process for choosing a state approach in a new React project.

I start with the most local option and promote only when pain is real. For state used by a single component, I use useState or useReducer. When siblings need the same data, I lift state to the nearest common parent. When state must cross many levels of the tree, I evaluate the change frequency: if it changes rarely (theme, auth, locale), I use Context. If it changes frequently and many components subscribe, I reach for an external library with fine-grained subscriptions — typically Zustand for its simplicity or Redux Toolkit for large teams that benefit from strict conventions. I never add a state library on day one unless the project requirements clearly demand it.


Quick Reference — Cheat Sheet

+-----------------------------------+-------------------------------------------+
| Concept                           | Key Point                                 |
+-----------------------------------+-------------------------------------------+
| Local state (useState)            | Used by one component. Keep it there.     |
|                                   | Form inputs, toggles, modals.             |
+-----------------------------------+-------------------------------------------+
| Lifted state                      | Siblings need the same data. Move state   |
|                                   | to nearest common parent, pass via props. |
+-----------------------------------+-------------------------------------------+
| Global state                      | Many unrelated components need it.        |
|                                   | Auth user, theme, cart, locale.           |
+-----------------------------------+-------------------------------------------+
| Context API                       | Dependency injection — broadcasts value.  |
|                                   | NOT a state manager. All subscribers      |
|                                   | re-render on any change.                  |
+-----------------------------------+-------------------------------------------+
| Context + useReducer              | Built-in "state management lite." Split   |
|                                   | state/dispatch contexts for performance.  |
|                                   | Enough for most apps.                     |
+-----------------------------------+-------------------------------------------+
| External library needed when      | Frequent updates + many subscribers.      |
|                                   | Fine-grained subscriptions required.      |
|                                   | Middleware for async side effects.         |
|                                   | DevTools / time-travel debugging.         |
|                                   | Complex multi-domain state.               |
+-----------------------------------+-------------------------------------------+
| Decision order                    | useState → lift to parent → Context →     |
|                                   | external library. Promote only when       |
|                                   | pain is real, not anticipated.            |
+-----------------------------------+-------------------------------------------+
| Form state rule                   | Transient state (typing) stays local.     |
|                                   | Committed state (submitted) goes global.  |
+-----------------------------------+-------------------------------------------+
| Context performance fix           | Split into multiple contexts by domain.   |
|                                   | Separate state context from dispatch.     |
|                                   | If still slow, switch to external lib.    |
+-----------------------------------+-------------------------------------------+

RULE: State should live as close as possible to where it is used.
RULE: Context is a delivery mechanism, not a state manager.
RULE: Start local. Promote only when the pain is real and measured.

Previous: Lesson 5.3 — Debounced Input & Search Patterns -> Next: Lesson 6.2 — Context API + useReducer Pattern ->


This is Lesson 6.1 of the React Interview Prep Course — 10 chapters, 42 lessons.

On this page