TL;DR
- Explain a Redux update as an event, a state transition and the specific UI data that changes.
- Use Redux Toolkit for normal application work, while understanding the immutable updates that its draft syntax produces.
- Keep local interaction state, shared client state and server-cache responsibilities distinct.
- Practice reducer tests, selector reference behavior and stale-request scenarios instead of memorizing definitions alone.
1. When would you choose Redux for a React application?
A useful answer begins with the application, not the library. Imagine a support dashboard where several views share selected tickets, filters and a multi-step bulk-action workflow. Ask which state must remain consistent across those views and which interactions stay local to one component.
Redux is one option for managing shared application state. Redux's overview recommends Redux Toolkit for writing Redux logic and identifies React-Redux as the React integration. It also explicitly notes that not every application needs Redux.
In an interview, explain the tradeoff: centralized transitions can make a complex shared workflow easier to inspect, but every text field does not need global ownership. A temporary tooltip or an unsaved search input may be simpler near the component that owns it. State your criteria before proposing a store. For a worked example, see how the Twitter home feed answer in this set of frontend engineer interview questions separates server state, cross-component UI state and local component state.
A strong follow-up question is, “Which screens need to agree on this value, and for how long?” The answer gives you more useful design information than asking whether the team likes Redux.
2. What happens when an action is dispatched?
Use a concrete event. A user marks ticket t1 as resolved. The UI dispatches an action describing that event; a reducer computes the next state; subscribers can then observe the updated state. The component should select the values it needs to display.
Be precise about responsibility. An action describes something that happened. The reducer decides the corresponding state change. Rendering turns the selected state into the interface. This explanation is more useful than repeating “one-way data flow” without connecting it to a user action.
Ask what should happen for an unknown ticket or a repeated resolution event. Those are product decisions. In the small exercise below, an unknown ticket and an already-resolved ticket leave the state unchanged. A real application might also record an audit event elsewhere, but the example does not invent one.
3. Can you implement and test an immutable update?
Here is an original plain reducer for the ticket exercise. It deliberately makes the changed references visible:
function ticketsReducer(state = { byId: {} }, action) {
if (action.type !== "tickets/resolved") return state;
const id = action.payload;
const ticket = state.byId[id];
if (!ticket || ticket.status === "resolved") return state;
return {
...state,
byId: {
...state.byId,
[id]: { ...ticket, status: "resolved" }
}
};
}Test more than the final word “resolved.” The old state should remain open, the changed ticket should have a new reference and an unrelated ticket should retain its existing reference. Calling the reducer with an unrelated action should return the original state object.
The immutable update guide explains why copying only the outer object is insufficient when a nested object changes. The example copies the ticket, its containing lookup and the outer state. It does not deep-clone every unrelated ticket.
For this exercise, assume internal actions contain a string ticket ID and the lookup contains trusted application records. Validate data when it enters the application; this reducer is not an untrusted HTTP request parser.
4. Why can Redux Toolkit use assignment syntax?
The corresponding Toolkit exercise is shorter:
import { createSlice } from "@reduxjs/toolkit";
const ticketsSlice = createSlice({
name: "tickets",
initialState: {
byId: {
t1: { id: "t1", status: "open" },
t2: { id: "t2", status: "open" }
}
},
reducers: {
resolved(state, action) {
const ticket = state.byId[action.payload];
if (ticket) ticket.status = "resolved";
}
}
});The createSlice reference explains that case reducers are wrapped with Immer and that slice actions are generated from the reducer definitions. Assignment here operates on a draft used to produce an immutable result. It is not permission to mutate arbitrary Redux state outside that mechanism.
Explain the difference while reviewing the code. Moving the assignment into an ordinary function that receives a live state object changes its meaning. Also keep the case reducer synchronous and focused on the transition; fetching a ticket belongs in an appropriate asynchronous workflow.
Run the same behavioral checks against both versions. A shorter implementation should preserve the requirements, not quietly redefine them.
5. Why might a selector trigger unnecessary renders?
Suppose the component only displays the status of t1. Selecting that primitive value is different from returning a newly constructed object on every call. In the latter case, even equal-looking contents can have different references.
React-Redux's hooks documentation states that useSelector uses strict reference equality by default when comparing results after dispatch. You can select separate primitive values, use a suitable memoized selector or deliberately choose an equality function when appropriate.
Do not claim that memoization prevents every render. Parent renders and other state changes still matter. First identify the selector result that changes, then measure whether the behavior is expensive enough to justify additional complexity.
An interview exercise is to compare a selector returning state.tickets.byId.t1.status with one returning { status: state.tickets.byId.t1.status }. Explain the difference before reaching for an optimization. Also ask whether derived values need to be stored at all when they can be computed from existing state.
6. How would you separate server data from editing state?
A ticket fetched from a service and a user's unsaved edit are related, but they are not the same thing. Consider what happens when the service refreshes while the user is editing. Blindly replacing the edit with the latest response may discard work.
Define ownership. The server response represents the last retrieved version. The local draft represents the user's pending change. A save operation needs a policy for conflicts, validation and failure. Explain which version the screen is displaying and what the user can retry.
RTK Query's cache behavior is based on endpoint definitions, serialized arguments and active subscriptions. Matching query keys share cached results; removing a component is not automatically an immediate deletion of that cache entry.
For the dashboard, two views requesting the same ticket can share a query result, while an unsaved note remains a separate draft. Include every relevant resource dimension in the data-access design. An organization's authorization must still be checked by the service; a frontend cache key is not an access-control boundary.
7. How would you handle responses arriving out of order?
Use a scenario rather than a slogan. A user selects ticket A and quickly selects ticket B. B's request finishes first; A's slower request arrives afterward. The UI must not show A's details under B's heading.
State the invariant: the displayed result belongs to the current selection. Then describe a design that preserves it, such as query state keyed by the requested ID, or a request identifier checked before applying a result in a custom workflow. Cancellation can reduce unnecessary work, but the display rule should remain correct even when a response arrives. The same invariant appears in the autocomplete question for UI system design rounds, where a sequence number lets you discard responses to an older query.
Test the order explicitly with controlled promises. Resolve B first, then A, and assert that B remains selected with B's data. Add a failure case where A errors after B succeeds. Do not let an old failure replace the current success message.
This is a design exercise, not a claim that the small reducer above implements network handling. Keep that boundary clear during an interview.
8. What would you investigate when state looks correct but the UI is wrong?
Trace one failing interaction from the event to the selected value. Record the action payload, the reducer result, the selector input and the rendered identifier. Predict each value before you record it, a habit that live debugging interview rounds reward. Avoid starting with a wholesale rewrite.
| Symptom | First question | Useful check |
|---|---|---|
| Wrong ticket changes | Did the action carry the intended ID? | Compare clicked row and payload |
| Old state appears modified | Was a nested reference mutated? | Freeze the previous state in a test |
| Repeated unrelated renders | Does the selector return a new reference? | Compare results for unchanged input |
| Stale response replaces current data | Which selection owns the response? | Resolve requests in reverse order |
| Local draft disappears | Did a refresh overwrite editing state? | Separate fetched data from pending edits |
For practice, explain one bug, its smallest correction and a test that would have caught it. Then explain a tradeoff you deliberately left unchanged. That demonstrates understanding more clearly than listing every Redux API.
The code examples here are deliberately small, original exercises. Use them to rehearse observable behavior, then adapt the questions to an application you actually built. Do not present a fictional support dashboard as your work experience.