Frontend interview practice question

When does a React app need Redux? Local state vs Context vs external stores

HighIntermediateReact

Interview quick answer

Redux is an external store with one-way data flow: components dispatch actions that describe what happened, pure reducers compute the next state, and each component re-renders only when the slice it selects changes. Most React state does not need it. Keep UI state local, lift it when siblings share it, use Context for values that rarely change, and give server data to a query cache. Reach for Redux Toolkit when many distant components share frequently changing client state with complex update rules, or when the team needs to trace every change.

Interview focus

This React interview question tests whether you can explain Redux vs Context vs useState: React Answer, connect it to production trade-offs, and handle common follow-up questions.

  • Redux vs Context vs useState: React Answer explanation without falling back to memorized definitions
  • State Management and Global State reasoning, edge cases, and production failure modes
  • How you would answer the most likely React interview follow-up
Practice more React interview questions
Interview answer drill

Use this React interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.

Full interview answer

The decision to explain

A dashboard keeps the signed-in user, the cart, the search box text and the fetched product list in one Context. The provider builds a new value object on every keystroke, so every consumer re-renders, including the header and the cart badge that never read the query. Someone proposes moving everything to Redux. A strong answer does not start with a library. It sorts the state by kind: the query is local UI state, the products are server data, the cart is shared client state, and the user is an app-wide value that rarely changes. Each kind has a cheaper home than a global store, and only what is left justifies Redux.

How Redux works

Redux keeps client state in one store outside the component tree. Components never change it directly. They dispatch an action, a plain object that describes what happened, such as { type: 'cart/itemAdded', payload: 'sku-42' }, and a pure reducer computes the next state from the current state and that action. After every dispatch, React-Redux re-runs each subscribed component's selector and re-renders only the components whose selected value changed by reference. Side effects such as requests live in middleware (thunks or listeners), never in reducers. The payoff is predictability: every change has a named cause that you can log, replay and inspect in Redux DevTools.

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

const cartSlice = createSlice({
  name: 'cart',
  initialState: { quantities: {} },
  reducers: {
    itemAdded(state, action) {
      const id = action.payload;
      // Immer turns this mutable-looking code into an immutable update
      state.quantities[id] = (state.quantities[id] ?? 0) + 1;
    },
  },
});

const { itemAdded } = cartSlice.actions;
const store = configureStore({ reducer: { cart: cartSlice.reducer } });

function CartBadge() {
  // Selects a number, so it re-renders only when the count changes
  const count = useSelector((state) =>
    Object.values(state.cart.quantities).reduce((sum, n) => sum + n, 0)
  );
  return <span aria-label={`${count} items in cart`}>{count}</span>;
}

function AddToCart({ id }) {
  const dispatch = useDispatch();
  return <button onClick={() => dispatch(itemAdded(id))}>Add to cart</button>;
}

export function App() {
  return (
    <Provider store={store}>
      <CartBadge />
      <AddToCart id="sku-42" />
    </Provider>
  );
}
                  

Kind of state

Example

Cheapest correct home

Local UI state

Input text, open or closed, active tab

useState or useReducer in the component that owns it

Shared by nearby components

A filter used by a list and its toolbar

Lift it to the closest common parent and pass props

App-wide, rarely changing

Theme, locale, signed-in user, feature flags

Context with a memoized value

Server data

Products, profile, search results

A query cache: TanStack Query, SWR or RTK Query

URL state

Page number, sort order, selected filters

Router search params, so links, reloads and the back button keep it

Form state

Field values, touched flags, errors

Local state or a form library; the Redux style guide advises keeping most form state out of Redux

Shared client state that changes often and has complex rules

Cart, multi-step editor, undo history, collaborative document state

An external store: Redux Toolkit, or a lighter store such as Zustand

Sort state by kind before you pick a library

Context is not a smaller Redux

Context only transports a value down the tree. The state itself still lives in a useState or useReducer above the provider. Context has no selectors: when the provider value changes by reference, every component that reads that context re-renders, whatever field it uses. That is fine for a theme or a locale. It hurts for values that change on every keystroke or every WebSocket message. Redux, Zustand and similar stores let each component subscribe to a slice and skip the render when that slice did not change. So the real Context-versus-Redux question is about update frequency and the number of subscribers, not about app size.

Production pitfall: selectors that return new references

useSelector runs your selector after every dispatched action and compares the result with ===. A selector that builds a new array or object therefore re-renders its component after every action anywhere in the app, even in unrelated slices. In development, React-Redux runs a selector twice on its first call and warns in the console when the two results differ, which is often the first hint of this bug.

JSX
import { createSelector } from '@reduxjs/toolkit';
import { useSelector } from 'react-redux';

// ❌ filter() returns a new array on every call, so this grid
// re-renders after every action dispatched anywhere in the app
function InStockGridSlow() {
  const items = useSelector((state) =>
    state.products.items.filter((product) => product.inStock)
  );
  return <ProductGrid items={items} />;
}

// ✅ createSelector returns the same array until state.products.items changes
const selectInStock = createSelector(
  [(state) => state.products.items],
  (items) => items.filter((product) => product.inStock)
);

function InStockGrid() {
  const items = useSelector(selectInStock);
  return <ProductGrid items={items} />;
}
                  

Server data is not global client state

Much of what teams used to put in Redux is a copy of server data plus hand-written loading, error and staleness flags. A query cache does that job better: it caches by key, deduplicates requests, refetches stale data and exposes loading and error states. Redux Toolkit includes RTK Query for this, and TanStack Query and SWR do the same without a store. Once server data moves out, the remaining global client state is often small enough for Context or a light store.

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

// The query cache owns fetching, caching, deduplication and refetching,
// so no products slice, status flag or error field is needed
function ProductList() {
  const { data, isPending, isError } = useQuery({
    queryKey: ['products'],
    queryFn: async () => {
      const response = await fetch('/api/products');
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return response.json();
    },
  });

  if (isPending) return <p>Loading products…</p>;
  if (isError) return <p role="alert">Products could not load.</p>;
  return <ProductGrid items={data} />;
}
                  

When Redux Toolkit is the right call

• Many components far apart in the tree read and update the same client state.
• That state changes often, and the update rules are complex enough to want pure, testable reducers.
• Several people work on the codebase and need one predictable pattern for changes.
• You need to see when and why state changed: action logs, time-travel debugging in Redux DevTools, or replaying the actions behind a bug report.

If none of these apply, Redux adds indirection (actions, reducers, selectors, a provider) without paying for it. The Redux FAQ itself says that not every app needs Redux.

Modern Redux means Redux Toolkit

Redux Toolkit is the officially recommended way to write Redux. configureStore adds the thunk middleware, connects Redux DevTools and, in development, checks for accidental mutations and non-serializable values. createSlice generates the action creators and lets reducers use Immer's mutable-looking syntax while still producing immutable updates. Hand-written switch reducers, action type constants and the legacy createStore setup are the old style. They were replaced because they needed a lot of boilerplate and made accidental mutations easy to ship.

Common mistakes

• Putting everything in the store, including input text and open or closed flags, so every keystroke goes through global code.
• Selecting the whole state, or building new objects and arrays inside selectors.
• Storing derived values such as a filtered list or a cart total instead of deriving them in selectors.
• Keeping non-serializable values such as promises, class instances or DOM nodes in state; the Redux style guide lists this as an essential rule.
• Copying server responses into slices and maintaining cache flags by hand.
• Treating Context plus useReducer as a drop-in Redux replacement for high-frequency updates.

Follow-up questions

Why must reducers be pure and updates immutable? React-Redux detects changes by reference: a new reference means the value changed. Pure reducers are also easy to test, and they make replaying actions in DevTools possible.

Zustand or Redux Toolkit? Both are external stores with selector-based subscriptions, and both can connect to Redux DevTools. Zustand has less ceremony. Redux Toolkit adds a fixed action-and-reducer structure, middleware and RTK Query. Choose by how much structure the team wants.

How does a store stay consistent during concurrent rendering? React-Redux has subscribed through React's useSyncExternalStore hook since version 8. A hand-rolled store read from a ref during render can show different values to components rendered in the same pass.

Interview framing

Say it like this:
"Redux is an external store with one-way data flow: components dispatch actions, pure reducers compute the next state, and each component re-renders only when the slice it selects changes. I don't start with it. I keep UI state local, lift it when siblings share it, use Context for values that rarely change and put server data in a query cache. I add Redux Toolkit when many distant components share frequently changing client state with complex update rules, or when the team needs to trace every change."

Summary

Redux is a predictable external store, not a default. Decide where state lives by its kind: local, lifted, Context, query cache, URL or form state. Use Redux Toolkit for shared client state that changes often and has complex rules, and keep selectors returning stable values, or the store will cause the re-renders it was meant to prevent.

Guides
Preparing for interviews?

Built in public

The code behind FrontendAtlas lives on GitHub. A star helps other frontend engineers find it.

Star on GitHub