Top Redux Interview Questions & Answers

Redux interviews cover the core principles of predictable state management, plus how Redux Toolkit has simplified what used to be a lot of boilerplate.

Take Practice Interview Now
Improve your chances of getting selected — rehearse these answers out loud with an AI interviewer and get a scored report in minutes.
Start Free →

1. What are the three core principles of Redux, and why do they matter?

Redux is built on three principles: single source of truth, state is read-only, and changes are made through pure functions. Single source of truth means your entire application state lives in one object tree inside a single `store`, which makes debugging and server-side rendering simpler because there's no hunting across scattered local state. State is read-only means you never mutate state directly; the only way to change it is to `dispatch` an action describing what happened. Changes are made with pure reducer functions, so given the same previous state and action, a reducer always produces the same next state, which is what makes time-travel debugging and predictable testing possible. Together these constraints trade a bit of boilerplate for predictability, which is exactly what large apps with many components touching shared state need.

2. What's the difference between an action and an action creator?

An action is a plain JavaScript object that describes something that happened in your app, and by convention it has a `type` field plus an optional `payload`. An action creator is just a function that returns an action object, so instead of writing the literal object everywhere you call something like `addTodo(text)` and it builds the object for you. Action creators exist mainly for convenience and consistency, keeping you from typo-ing a `type` string in ten different places and making it easy to bake in logic like generating an `id` or timestamp. In Redux Toolkit, `createSlice` auto-generates action creators for you based on the reducer names you define. It's worth remembering an action creator itself doesn't dispatch anything, you still have to pass its return value into `dispatch`.

3. What is a reducer, and why must it be a pure function?

A reducer is a function with the signature `(state, action) => newState` that decides how state changes in response to a dispatched action. It has to be pure because Redux relies on predictability, calling the same reducer with the same state and action should always produce the identical result, with no side effects like API calls, mutating the arguments, or reading from `Date.now()` or `Math.random()`. Purity is also what enables features like Redux DevTools time-travel, since replaying the same sequence of actions must always reconstruct the same state. If a reducer mutated state in place or produced different output for identical input, the change-detection systems in `connect` and `useSelector`, which rely on reference equality, would break silently. That's why side effects like fetching data belong in middleware, not in the reducer itself.

4. What is the Redux store, and what actually happens when you call dispatch?

The store is the object that holds the entire state tree and exposes a small API: `getState`, `dispatch`, and `subscribe`. There's only one store per app, which is what "single source of truth" refers to under the hood. When you call `dispatch(action)`, the store passes the current state and that action into your root reducer, which returns a new state object, and the store swaps its internal reference to that new object. After swapping, it synchronously notifies every subscriber, which is how UI bindings like `useSelector` know to re-render, since they've subscribed under the hood and compare the new state slice against the old one. Dispatch itself is intentionally synchronous and simple; any async behavior you see is actually middleware intercepting the action before it reaches the reducer.

5. What is middleware in Redux, and why do we need something like thunk or saga for async logic?

Middleware sits between `dispatch` and the reducer, forming a pipeline that lets you intercept, inspect, delay, or cancel an action before it reaches the store. It's implemented as a curried function, `store => next => action => {}`, which is how multiple middlewares get chained together. We need it for async logic because a reducer has to stay synchronous and pure, so something has to sit in front of the store to let you dispatch a "thunk" or trigger a saga that performs the async work and dispatches plain actions once it resolves. `redux-thunk` is the simplest version, it just lets an action creator return a function instead of an object, and that function receives `dispatch` and `getState` so it can dispatch further actions after an `await`. `redux-saga` takes a heavier but more powerful approach using generator functions to describe complex async flows, like cancellation, debouncing, or racing requests, in a way that's easy to test in isolation.

6. What is Redux Toolkit, and why is it the recommended way to write Redux now?

Redux Toolkit, usually called RTK, is the official, opinionated package for writing Redux logic, and the Redux team now considers it the default way to use Redux rather than an optional add-on. It solves the classic complaints about boilerplate: `configureStore` sets up the store with good defaults like thunk middleware and DevTools already wired in, and `createSlice` generates action types, action creators, and a reducer all from one object, so you write far less code. It also bundles Immer internally, so inside a slice's reducer you can write what looks like direct mutation, `state.items.push(newItem)`, and Immer safely converts that into an immutable update behind the scenes. On top of that it ships `createAsyncThunk` for standardized async action handling and RTK Query for data fetching and caching, covering most of what people used to reach for thunk or saga plus a separate fetching library to handle. It keeps the same core Redux principles but removes almost all the ceremony around them.

7. How does Redux differ from React's built-in Context API?

Context is a dependency-injection mechanism for passing data down the tree without prop drilling, it's not really a state management system, it has no built-in concept of actions, reducers, middleware, or DevTools. Redux is a full state container with a predictable update pattern, a single store, and a rich ecosystem of middleware and debugging tools. A key practical difference is re-renders: when a Context value changes, every component consuming that context re-renders regardless of which part of the value it actually uses, whereas `useSelector` in Redux lets each component subscribe to just the slice of state it needs and skip re-rendering if that slice is reference-equal to before. Context is great for low-frequency, mostly-static data like theme or the authenticated user, but it starts to hurt performance and gets hard to reason about once you're managing frequently-updating, deeply interconnected application state. In practice plenty of apps use both, Context for simple static values and Redux for complex shared state that many components read and write.

8. What are selectors, and why would you memoize them with something like reselect?

A selector is just a function that takes the Redux state and returns some derived piece of it, for example `selectCompletedTodos(state)` filtering the todos array down to completed ones. The problem is that a `filter` or `map` creates a brand-new array or object every time it runs, so if that logic runs directly inside `useSelector` on every render, you get a new reference each time even when the underlying data hasn't changed, which breaks the reference-equality check `useSelector` uses to decide whether to re-render. `reselect`'s `createSelector` fixes this through memoization, caching the last inputs and result and only recomputing when the specific input selectors it depends on actually change. That means expensive derived computations run once instead of on every dispatch, and consuming components don't re-render because they receive the exact same cached reference back. Redux Toolkit re-exports `createSelector` directly, so you don't even need reselect as a separate dependency anymore.

9. What's the difference between the old connect/mapStateToProps pattern and the useSelector/useDispatch hooks?

`connect` is a higher-order component, you wrap your component with `connect(mapStateToProps, mapDispatchToProps)(MyComponent)`, and it injects the selected state and bound action creators as props. `useSelector` and `useDispatch` are hooks that let a function component read from the store and dispatch actions directly, without the extra wrapping component or a separate `mapStateToProps` function defined up front. Functionally they achieve the same subscription and re-render behavior, but the hooks are less boilerplate-y and fit more naturally with how modern function components are written. One subtle difference is that `mapStateToProps` recomputes all its mapped values together as one merged object comparison, while each `useSelector` call is an independent subscription, so a component using several `useSelector` calls can be more granular about what actually triggers a re-render. The Redux docs recommend hooks for new code; `connect` still works and isn't deprecated, but it's mostly seen in older codebases today.

10. Why does Redux require immutability, and how do people actually enforce it?

Redux's change detection is built entirely around reference equality, both `connect` and `useSelector` decide whether to re-render by checking if the new state slice is `===` the old one, which is a cheap shallow check. If you mutated state in place, the reference would stay the same even though the contents changed, so the UI simply wouldn't update, producing bugs that are maddening to track down because the data actually did change in memory. That's why reducers must always return a new object or array for anything that changed, using patterns like the spread operator, `{ ...state, count: state.count + 1 }`, instead of assigning into the existing object. Immutability is also what makes time-travel debugging and undo-redo possible, since every past state is a distinct, untouched object you can jump back to. Writing this by hand is error-prone with deeply nested state, which is exactly why Redux Toolkit bundles Immer, letting you write mutating-looking code inside `createSlice` that gets converted into proper immutable updates automatically.

11. What does normalizing state shape mean, and why bother doing it?

Normalizing means structuring your state more like a relational database than a nested tree, so instead of an array of post objects that each embed their author and a nested array of comments, you store separate lookup tables keyed by id, typically shaped like `{ byId: {...}, allIds: [...] }` per entity type, with relations referenced by id. The main reason is that deeply nested duplicated data is painful to update immutably and easy to get out of sync, if the same user object is embedded in fifty different posts and their name changes, you'd have to find and update it in fifty places. With a normalized shape you update the one entry in the `users` table and every place referencing that id reflects the change automatically. It also makes lookups by id O(1) instead of searching through nested arrays, and it plays nicely with selectors that reassemble a denormalized view for a specific component. Redux Toolkit ships `createEntityAdapter` specifically to generate the boilerplate for this `byId`/`allIds` pattern along with prebuilt selectors and CRUD reducers.

12. What's the practical difference between redux-thunk and redux-saga?

`redux-thunk` lets an action creator return a function instead of a plain object, and that function gets `dispatch` and `getState` injected, so you write async logic imperatively with `async/await`, dispatching a pending action, awaiting a fetch call, then dispatching a fulfilled or rejected action. `redux-saga` instead uses ES6 generator functions and declarative "effects" like `call`, `put`, and `takeLatest`, so instead of calling the API directly and dispatching, you `yield` descriptions of what should happen and the saga middleware executes them. Thunk is simpler, has almost no learning curve, and is what `createAsyncThunk` in Redux Toolkit is built on, so it covers the vast majority of everyday async needs. Saga shines in more complex scenarios, cancelling an in-flight request when a component unmounts, debouncing a search input, or coordinating async flows that race against each other, because generators are naturally interruptible and effects are easy to test since they're plain descriptive objects rather than executed side effects. The tradeoff is saga has a steeper learning curve and more setup, so most teams reach for it only once thunks genuinely aren't expressive enough.

13. How does Redux DevTools time-travel debugging actually work?

The DevTools extension hooks into the store and keeps a log of every dispatched action alongside a snapshot of the resulting state. Because reducers are pure functions, replaying that log from the initial state deterministically reproduces the exact same sequence of states, which is what makes time travel possible, you can drag a slider back to any point in the action history and the DevTools re-renders the app using the state that existed at that moment. Under the hood, jumping to a past point isn't rewinding anything magical, it's literally re-running your root reducer over the recorded actions up to that index, or directly injecting that historical state snapshot back into the store. This is also why side effects in reducers are dangerous, if a reducer called an API or mutated shared data, replaying actions for time travel would re-trigger those effects or produce different results, breaking the whole illusion. It's an enormously useful debugging tool in practice, seeing the exact diff of state before and after every single action makes tracking down "why did this value change" bugs dramatically faster.

14. What is combineReducers, and when would you reach for it?

`combineReducers` takes an object whose keys are state slice names and whose values are individual reducer functions, and returns one root reducer managing the combined shape, so state ends up looking like `{ users: usersReducer(...), posts: postsReducer(...) }`. It exists so that as an app grows, you can write focused reducers that each only think about their own slice, and let `combineReducers` correctly return a new top-level object when any slice actually changes and the same object reference when nothing underneath changed. You'd reach for it any time your app has more than one loosely-related domain of state, todos and user settings for example, rather than cramming everything into one giant reducer with a huge switch statement. Redux Toolkit's `configureStore` accepts a `reducer` option that's just an object of slice reducers and calls `combineReducers` internally for you, so most people today use it indirectly through `configureStore` rather than importing it by hand. One gotcha worth knowing is that each slice reducer only ever sees its own slice of state, not the whole tree, so if logic genuinely needs data from another slice, that generally has to move to a selector or middleware instead.

15. What are some common Redux performance pitfalls, and how do you avoid them?

The most common one is calling `useSelector` with an inline function that creates a new array or object on every render, like `useSelector(state => state.todos.filter(t => t.done))`, since that produces a new reference every time regardless of whether the underlying data changed, forcing the component to re-render on every dispatch anywhere in the app. The fix is to memoize that derived value with `reselect`'s `createSelector` so it only recomputes and returns a new reference when its actual inputs change. Another common issue is selecting the entire state object in one `useSelector` call instead of just the slice a component needs, which makes that component re-render on unrelated state changes elsewhere in the store. Overly deep or badly normalized state can also hurt, since spreading deeply nested objects immutably on every update gets both verbose and comparatively slow, which is part of why normalizing state and leaning on Immer through Redux Toolkit helps. Lastly, dispatching too many fine-grained actions in a tight loop, say inside a scroll or resize handler, without batching or debouncing can flood the app with re-renders, so throttling the dispatch or batching related updates into a single action is usually the right move.

Take Practice Interview Now
Improve your chances of getting selected — rehearse these answers out loud with an AI interviewer and get a scored report in minutes.
Start Free →
Related topics
ReactAngularVue.jsNext.js