ForgeFrontend - Prepare, Practice, Crack
Secure checkout
Lifetime access
Instant PDF download
Free updates forever

Prepare · Practice · Crack

React Redux Interview Questions and Answers

Short answer

Redux interviews now assume Redux Toolkit. The recurring questions are the one-way data flow, why reducers must be pure, how Immer allows mutating syntax, what middleware does, why selectors are memoised, and, most important, when you should not use Redux at all.

The questionWhat it reveals
Explain the data flowWhether you can describe it without hand-waving
Why must reducers be pure?Whether you know changes are detected by reference
What does createSlice replace?Whether you have written Redux before Toolkit
How does Immer work?Whether "mutating" code confuses you or not
What is middleware?Whether dispatch is a black box to you
Why memoise a selector?Real performance understanding, not recital
How would you store a list?Normalisation - the most common design mistake
When would you NOT use Redux?Seniority. The best answer is "most of the time"
What each Redux question is really testing. The last row is the one that decides senior interviews.

What Redux interviews actually ask now

Two things have changed and both show up in interviews. Redux Toolkit is no longer optional, it is the official way to write Redux, and describing hand-written action-type constants and switch statements as your default marks you as out of date rather than experienced. And a large share of what teams used Redux for, caching server responses, has moved to React Query or RTK Query, so the interesting question is no longer how Redux works but what is left for it to do.

That is why the strongest answers here are scoped rather than enthusiastic. An interviewer who asks "do you know Redux?" is often really asking "will you reach for Redux on day one of a project that does not need it?" Knowing the internals and knowing when to leave them alone are both being assessed, and the second is what separates the candidates.

What are the core Redux concepts?

One store holds all the state as a single object tree. An action is a plain object describing what happened, with a `type` and usually a `payload`. A reducer is a pure function taking the current state and an action and returning the next state. Components dispatch actions and read state through selectors. Data flows one way - dispatch, reducer, new state, re-render, and nothing writes to the store directly.

A complete slice, the modern waytypescript
1import { createSlice, configureStore } from "@reduxjs/toolkit";
2
3const cartSlice = createSlice({
4  name: "cart",
5  initialState: { items: [] as string[] },
6  reducers: {
7    added(state, action) {
8      state.items.push(action.payload);   // looks like mutation, see Immer below
9    },
10    removed(state, action) {
11      state.items = state.items.filter((i) => i !== action.payload);
12    },
13  },
14});
15
16export const { added, removed } = cartSlice.actions;
17const store = configureStore({ reducer: { cart: cartSlice.reducer } });
18
19store.dispatch(added("react-kit"));
20console.log(store.getState().cart.items);
21// → [ 'react-kit' ]
22
23console.log(added("js-kit"));
24// → { type: 'cart/added', payload: 'js-kit' }
25// → the action type is generated from the slice name; no constants file

Being able to name what this replaces is worth doing unprompted, because it shows the boilerplate reduction is something you have felt rather than read about. `createSlice` generates the action creators and the type strings, removes the constants file and the switch statement, and `configureStore` sets up the DevTools extension, the thunk middleware and development-only checks for accidental mutation and non-serialisable values, all of which used to be manual wiring.

Why must reducers be pure and immutable?

Because change detection is a reference comparison. React-Redux decides whether a component needs to re-render by comparing the previous selected value with the next one using strict equality, so mutating an object in place leaves the reference identical and the update is invisible. Purity also buys predictability - the same state and action always produce the same result, which is what makes time-travel debugging and replaying actions possible at all.

Why the mutating version silently does nothingjavascript
1// Mutation: same reference, so nothing re-renders
2function badReducer(state, action) {
3  state.items.push(action.payload);
4  return state;
5}
6const before = { items: ["a"] };
7const after = badReducer(before, { payload: "b" });
8console.log(after === before);
9// → true        connected components see no change at all
10
11// Immutable: a new object at every level that changed
12function goodReducer(state, action) {
13  return { ...state, items: [...state.items, action.payload] };
14}
15const next = goodReducer(before, { payload: "c" });
16console.log(next === before, next.items === before.items);
17// → false false
18
19// And what "pure" excludes
20function impure(state, action) {
21  state.updatedAt = Date.now();   // → different result for identical inputs
22  fetch("/log");                  // → a side effect inside a reducer
23  return state;
24}
25// → both break time-travel: replaying the action gives a different outcome

The follow-up is where side effects belong, and the answer is middleware, a thunk, a listener, or RTK Query. A reducer that fetches, logs, reads `Date.now()` or generates a random id is not replayable, which defeats the entire reason for the pattern. Generating the id in the action creator, or in `createSlice`'s `prepare` callback, is the idiomatic fix.

How does Immer let you write mutating code?

Redux Toolkit passes your reducer a draft, a Proxy that records every write you make instead of applying it. When the reducer returns, Immer replays those recorded changes to produce a new state object, structurally sharing every branch you did not touch. So the syntax is mutable and the result is immutable, and the parts of the tree you left alone keep their references, which is exactly what memoised selectors and `React.memo` need.

The one rule: mutate the draft or return a value, never bothjavascript
1import { produce } from "immer";
2
3const state = { user: { name: "A" }, items: ["x"] };
4
5const next = produce(state, (draft) => {
6  draft.items.push("y");
7});
8
9console.log(next.items);              // → [ 'x', 'y' ]
10console.log(state.items);             // → [ 'x' ]        original untouched
11console.log(next.user === state.user);
12// → true      structural sharing: the untouched branch keeps its reference
13
14// The mistake that breaks a slice reducer
15// reducers: {
16//   wrong(state, action) {
17//     state.items.push(action.payload);
18//     return { ...state, done: true };   // → Error: may not both mutate and return
19//   }
20// }
21
22// Reassigning the whole draft does NOT work, the local name just moves
23const noop = produce(state, (draft) => {
24  draft = { user: {}, items: [] };
25});
26console.log(noop === state);          // → true     nothing changed

Both mistakes in that snippet come up in real review. Mutating the draft and also returning a value throws, because Immer cannot tell which one you meant. Reassigning `draft` itself does nothing, for the same pass-by-value reason that reassigning a function parameter does nothing, you have to write `state.items = []` rather than `state = { items: [] }`. And Immer's mutable syntax only applies inside a Toolkit reducer or a `produce` call; a plain reducer still needs spreads.

What is middleware, and how does a thunk work?

Middleware sits between `dispatch` and the reducer, forming a chain that each dispatched action passes through. Every middleware can inspect the action, transform it, delay it, log it, or stop it from continuing. It is the extension point where everything that is not a pure state transition lives, logging, analytics, and above all asynchronous work.

Middleware in twelve lines, and what thunk addsjavascript
1// The signature everyone is asked to reproduce
2const logger = (store) => (next) => (action) => {
3  console.log("dispatching", action.type);
4  const result = next(action);        // pass it along the chain
5  console.log("next state", store.getState());
6  return result;
7};
8
9// Thunk is barely bigger: if the "action" is a function, call it
10const thunk = (store) => (next) => (action) =>
11  typeof action === "function"
12    ? action(store.dispatch, store.getState)
13    : next(action);
14
15// Which is what makes this possible
16const loadUser = (id) => async (dispatch) => {
17  dispatch({ type: "user/loading" });
18  try {
19    const res = await fetch("/api/users/" + id);
20    dispatch({ type: "user/loaded", payload: await res.json() });
21  } catch (e) {
22    dispatch({ type: "user/failed", payload: e.message });
23  }
24};
25// → dispatch(loadUser(1)) dispatches three plain actions over time
26// → the reducer still only ever sees plain objects

The curried triple-arrow signature is a standard whiteboard question and worth being able to write from memory. The insight to state alongside it is that thunk does not extend Redux so much as bypass it briefly: the store still only ever receives plain, serialisable actions, and the async work happens outside the reducer where it belongs. `createAsyncThunk` wraps this pattern and generates the pending, fulfilled and rejected actions for you.

This is 1 of 75+ questions in the React Interview Kit

Get every question with detailed answers, follow-ups and real code, plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the React Interview Kit → ₹399

What are selectors, and why memoise them?

A selector is a function from the store state to the piece a component needs. Keeping them in one place means components do not depend on the shape of the state tree, so reshaping the store does not touch every component. Memoisation matters because `useSelector` re-runs after every dispatch and compares the result by reference, a selector that builds a new array or object every time returns a new reference every time, so the component re-renders on every action in the application.

The re-render bug, and the two fixesjavascript
1import { createSelector } from "@reduxjs/toolkit";
2
3// Re-renders on EVERY dispatch: filter returns a new array each call
4// const done = useSelector((s) => s.todos.filter((t) => t.done));
5
6// Fix 1: createSelector caches while the inputs are reference-equal
7const selectDone = createSelector(
8  [(s) => s.todos],
9  (todos) => todos.filter((t) => t.done)
10);
11// → recomputes only when s.todos itself changes identity
12
13// Fix 2: for a plain object of primitives, compare shallowly
14// const { name, email } = useSelector(selectUser, shallowEqual);
15
16const state1 = { todos: [{ id: 1, done: true }] };
17console.log(selectDone(state1) === selectDone(state1));
18// → true       same input, cached output
19
20const state2 = { todos: state1.todos };
21console.log(selectDone(state2) === selectDone(state1));
22// → true       the todos reference is unchanged, so the cache still holds
23
24// The catch: the default cache size is one
25console.log(selectDone({ todos: [] }).length, selectDone(state1).length);
26// → 0 1        alternating inputs recompute every time

The cache-size-of-one detail is the advanced follow-up. A selector shared by a list of components each passing a different id will thrash, because every call evicts the previous result, the fix is a selector factory created per component instance, or `createSelector` with a larger cache. Being able to describe that failure is a genuinely senior answer and very few candidates reach it.

How should state be shaped, and what is normalisation?

Store collections as a lookup object keyed by id plus an array of ids, rather than as a nested array of objects. Updating one item then means writing to one key instead of mapping the whole array, lookups are constant time instead of linear, and the same entity is not duplicated in three places waiting to go out of sync.

Nested versus normalisedjavascript
1// Nested: the same user object copied into every post
2const nested = {
3  posts: [
4    { id: "p1", title: "A", author: { id: "u1", name: "Arun" } },
5    { id: "p2", title: "B", author: { id: "u1", name: "Arun" } },
6  ],
7};
8// → renaming u1 means finding and updating every copy
9
10// Normalised
11const normalised = {
12  posts: { byId: { p1: { id: "p1", title: "A", author: "u1" },
13                   p2: { id: "p2", title: "B", author: "u1" } },
14           allIds: ["p1", "p2"] },
15  users: { byId: { u1: { id: "u1", name: "Arun" } }, allIds: ["u1"] },
16};
17
18console.log(normalised.users.byId.u1.name);   // → Arun     one place to update
19console.log(normalised.posts.allIds.map((id) => normalised.posts.byId[id].title));
20// → [ 'A', 'B' ]      allIds preserves order

`createEntityAdapter` in Redux Toolkit generates this shape along with the add, update, remove and upsert reducers and a set of selectors, which is the answer to give if asked how you would implement it. The `allIds` array is worth explaining too, an object's key order is not something to rely on for display order, so the array is what preserves it.

What is RTK Query, and when does it replace thunks?

RTK Query is a data-fetching and caching layer built into Redux Toolkit. You declare endpoints and it generates hooks that handle the request, caching, deduplication of identical in-flight requests, loading and error state, cache invalidation by tag, and refetching. It replaces the entire hand-written thunk-plus-slice pattern for server data, which in most applications was the bulk of the Redux code.

The framing that lands in an interview is that server data was never really client state. It is a cache of something owned elsewhere, with its own concerns, staleness, revalidation, retries, that a reducer was never designed to express. Once RTK Query or React Query owns that, the Redux store is left with genuine client state: the open modal, the multi-step form draft, the selected rows, the theme. Some teams find that remainder small enough that they drop Redux entirely, and saying so is not disloyal, it is the current consensus.

When do you actually need Redux?

When several distant parts of the application read and write the same frequently-changing client state, and you need the tooling, a serialisable action log, time-travel debugging, middleware, and predictable updates that a team can reason about. Large applications with complex cross-cutting interactions genuinely benefit. A dashboard with a handful of screens does not.

Split the decision by the kind of state rather than by its scope. Server data goes to a query cache. URL state - filters, pagination, the selected tab, belongs in the URL, where it survives a refresh and can be shared as a link. Form state belongs in the form library. What is left is the candidate for a store, and for that remainder Context is fine when it changes rarely, Zustand is fine when it changes often and you want minimal ceremony, and Redux earns its place when the tooling and the discipline are worth the boilerplate. The two comparison pages linked below go deeper on each.

What loses marks in a Redux round

The failure modes here are unusually consistent, and most are about being a version behind.

  • →Describing hand-written constants and switch statements as your normal approach.
  • →Not knowing that Toolkit reducers only look mutable because of Immer.
  • →Saying reducers must be pure without connecting it to reference-based change detection.
  • →Putting fetches, Date.now() or random ids inside a reducer.
  • →Never having memoised a selector, or not knowing why an inline filter re-renders.
  • →Storing a list as a nested array and then mapping it on every update.
  • →Answering "when would you use Redux?" with "for global state" and stopping there.

Frequently asked questions

What are the core concepts of Redux?
A single store holding state as one object tree, plain action objects describing what happened, pure reducers that take state and an action and return the next state, and selectors that read from the store. Data flows one way: dispatch, reducer, new state, re-render. Nothing mutates the store directly.
Why must Redux reducers be pure?
Because React-Redux detects change by comparing references, so mutating state in place leaves the reference identical and no component re-renders. Purity also makes actions replayable, which is what enables time-travel debugging. Fetches, timestamps and random ids belong in middleware or action creators, never in a reducer.
What is Redux Toolkit and why is it recommended?
The official, batteries-included way to write Redux. createSlice generates action creators and type strings and removes the constants file and switch statement, configureStore wires up DevTools, thunk middleware and development-time mutation checks, and Immer allows mutating syntax that produces immutable results.
How does Immer allow mutating code in a reducer?
It hands your reducer a Proxy draft that records writes rather than applying them, then replays those changes onto a new state object while structurally sharing untouched branches. So the code reads as mutation and the output is a new immutable state whose unchanged parts keep their references.
Can I mutate the draft and also return a value from a slice reducer?
No, Immer throws, because it cannot tell which result you intended. Either mutate the draft and return nothing, or build and return a new state without touching the draft. Reassigning the draft parameter itself also does nothing; assign to its properties instead.
What is Redux middleware?
A function with the signature store => next => action that sits between dispatch and the reducer. Each middleware can inspect, transform, delay or block an action before passing it on with next(action). It is where logging, analytics and asynchronous work live, so reducers stay pure.
How does redux-thunk work?
It is a middleware that checks whether the dispatched value is a function; if so it calls it with dispatch and getState instead of passing it to the reducer. That lets an async function dispatch several plain actions over time, loading, then success or failure, while the store still only ever receives serialisable objects.
Why do selectors need to be memoised?
useSelector re-runs after every dispatch and compares the result by reference. A selector that calls filter or map builds a new array each time, so the reference always differs and the component re-renders on every action anywhere in the app. createSelector caches the result while its inputs are unchanged.
What is normalised state in Redux?
Storing collections as a byId lookup object plus an allIds array rather than as nested arrays of objects. Updates touch one key instead of remapping an array, lookups are constant time, and an entity exists in exactly one place so copies cannot drift. createEntityAdapter generates this shape and its reducers.
What is RTK Query and does it replace thunks?
A data-fetching and caching layer in Redux Toolkit that generates hooks from endpoint definitions and handles caching, request deduplication, loading and error state, tag-based invalidation and refetching. For server data it replaces the whole hand-written thunk-and-slice pattern; thunks remain useful for other side effects.
Is Redux still used in 2026?
Yes, widely, particularly in large and long-lived applications, but with Redux Toolkit rather than hand-written boilerplate, and usually alongside a query library for server data. Many new projects reach for Zustand or Context instead, so the useful skill is knowing which problem each one solves.
When should I not use Redux?
When the state is server data, which belongs in a query cache; when it is filters or pagination, which belong in the URL; when it is form state, which belongs in the form; and when it is local to one component subtree. Once those are separated, many applications have too little genuinely global client state to justify a store.

This is 1 of 75+ questions in the React Interview Kit

Get every question with detailed answers, follow-ups and real code, plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the React Interview Kit → ₹399

Full kit

React Interview Kit · ₹399

Get it →