Redux interviews test the mental model as much as the API — why state is immutable, why reducers must be pure, why middleware exists at all. This covers all 100 questions, organized by topic, with code (mostly modern Redux Toolkit, with the classic hand-written patterns explained alongside since interview banks and older codebases still ask about them directly), a diagram, and a mock test at the end.
Redux Fundamentals
Q1. What is Redux and how is it used in web development?
Redux is a predictable state container — a single, centralized store holding an application's entire state, updated only through dispatched actions handled by pure reducer functions. It's used (most commonly alongside React, though it's framework-agnostic) when an application's state is complex enough, or shared across enough distant components, that prop-drilling or scattered local state becomes genuinely hard to reason about.
Q2. Can you describe the three principles that Redux is based upon?
| Principle | Meaning |
|---|---|
| Single source of truth | The entire application state lives in one store |
| State is read-only | The only way to change state is to dispatch an action |
| Changes are made with pure functions | Reducers take the previous state + an action, return a new state, with no side effects |
Q3. What is an action in Redux?
{ type: "todos/add", payload: { text: "Buy milk" } }An action is a plain JavaScript object describing something that happened — a required type field (conventionally a string) identifying what kind of event it is, plus any additional data (usually under payload) the reducer needs to compute the new state.
Q4. How are actions used to change the state in a Redux application?
store.dispatch({ type: "todos/add", payload: { text: "Buy milk" } });
// The store runs the root reducer with (currentState, action), replacing state with whatever it returnsDispatching an action is the only entry point for changing state — the store passes the action (and the current state) to the reducer, and whatever the reducer returns becomes the new state, triggering a re-render of any subscribed UI.
Q5. What is a reducer in Redux and what role does it play?
function todosReducer(state = [], action) {
switch (action.type) {
case "todos/add":
return [...state, action.payload];
default:
return state; // unknown action — return state unchanged
}
}A reducer is a pure function — (previousState, action) => newState — with no side effects, no mutation of its arguments, and no randomness/dates/API calls inside it. Purity is what makes Redux state predictable and enables features like time-travel debugging, since replaying the same actions against the same starting state always produces the same result.
Q6. How does Redux differ from local component state in React?
| Local component state (useState) | Redux | |
|---|---|---|
| Scope | Confined to one component (and its children, if passed down) | Global — accessible from anywhere without prop drilling |
| Sharing across distant components | Requires lifting state up, or Context | Any connected component can read/dispatch directly |
| Overhead | None — built into React | Store setup, actions, reducers — real ceremony for simple cases |
Local state is the right default for anything genuinely local to one component/subtree; Redux earns its overhead when state is truly shared across many unrelated parts of the app, or when you want strict, centralized, inspectable state transitions.
Q7. Define 'store' in the context of Redux.
import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({ reducer: rootReducer });
store.getState();
store.dispatch(someAction);
store.subscribe(() => console.log("state changed"));The store holds the application's entire state tree, and exposes getState() (read the current state), dispatch(action) (the only way to change it), and subscribe(listener) (react to changes) — it's a single object per application, not one per feature/component.
Q8. Can you describe the concept of 'single source of truth' in Redux?
Every piece of shared application state lives in exactly one place — the store — rather than duplicated or synchronized across multiple components' local state. This eliminates an entire category of bugs where two copies of "the same" data drift out of sync with each other.
Q9. How do you create a Redux store?
// Modern (Redux Toolkit) — the recommended approach today
import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({ reducer: { todos: todosReducer } });
// Classic (plain Redux) — what older codebases and many interview banks still describe
import { createStore } from "redux";
const store = createStore(rootReducer);Q10. What is meant by 'immutable state,' and why is it important in Redux?
// Wrong — mutates the existing array
state.push(newTodo);
return state;
// Correct — returns a NEW array, leaving the original untouched
return [...state, newTodo];Immutability means a reducer never modifies the existing state object — it always returns a new one. This matters because React-Redux (and Redux DevTools' time-travel) detect changes via reference equality — if state were mutated in place, the reference wouldn't change, and connected components would never know to re-render.
Q11. Explain the significance of the combineReducers function.
import { combineReducers } from "redux";
const rootReducer = combineReducers({
todos: todosReducer,
user: userReducer,
});
// state shape becomes: { todos: [...], user: {...} }combineReducers lets each feature/domain have its own independent reducer managing its own slice of state, then combines them into one root reducer matching the overall state shape — so no single reducer needs to know about or handle the entire application's state.
Q12. What are pure functions and side effects in the context of Redux?
A pure function's output depends only on its inputs, with no observable side effects (no API calls, no mutating external variables, no randomness) — reducers must be pure for exactly the reasons covered above (predictability, time-travel debugging). Side effects (API calls, timers, logging) are real and necessary in any app, but Redux deliberately pushes them out of reducers and into middleware (thunks, sagas) instead.
Redux Core Concepts
Q13. How do you handle asynchronous actions in Redux?
// Redux Toolkit's createAsyncThunk — the modern standard approach
import { createAsyncThunk } from "@reduxjs/toolkit";
export const fetchTodos = createAsyncThunk("todos/fetch", async () => {
const res = await fetch("/api/todos");
return res.json();
});Since a reducer can't perform a side effect itself, async logic (an API call) lives in middleware — dispatching a "pending" action, then the real data once it resolves (or a "rejected" action on failure), with reducers only ever handling those plain, synchronous action objects.
Q14. What is a 'selector' in Redux and what is its purpose?
export const selectCompletedTodos = (state) =>
state.todos.filter((t) => t.completed);
const completed = useSelector(selectCompletedTodos);A selector is a function that extracts (and often derives/computes) a specific piece of data from the store's state — centralizing "how to read this data" in one place, so components don't each independently know the exact shape of the state tree, and so that logic can be memoized (covered later, via reselect) for performance.
Q15. How does Redux handle the flow of data and actions?
The Redux unidirectional data flow
UI event
A user interaction dispatches an action
Reducer
Pure function: (state, action) → new state
Store
Holds the new state, notifies subscribers
UI re-renders
Connected components re-render with the new state
Data flows in one direction only — actions flow into reducers, reducers produce new state, the store notifies subscribers, and the UI reads the new state and re-renders. There's no back-channel where a component directly mutates state; everything is mediated through this same predictable cycle.
Q16. Can you explain the difference between a Container and Presentational component in Redux?
| Presentational component | Container component | |
|---|---|---|
| Concerned with | How things look — pure UI, receives data via props | How things work — connected to the store, passes data down as props |
| Redux awareness | None — completely Redux-agnostic | Directly reads/dispatches to the store |
This separation predates React Hooks — with the modern useSelector/useDispatch hooks, any component can read/dispatch directly without a dedicated wrapper, so the strict container/presentational split is less rigidly enforced today, though the underlying principle (keep pure UI components decoupled from data-fetching concerns) still holds up as good practice.
Q17. What is middleware in Redux and when would you use it?
const logger = (store) => (next) => (action) => {
console.log("dispatching", action);
const result = next(action);
console.log("new state", store.getState());
return result;
};Middleware intercepts every dispatched action before it reaches the reducer, able to log it, delay it, transform it, or trigger side effects (an API call) in response — the standard mechanism for anything that shouldn't live inside a pure reducer: async logic, logging, crash reporting.
Q18. How do you access the Redux store in a React component?
import { useSelector, useDispatch } from "react-redux";
function TodoList() {
const todos = useSelector((state) => state.todos);
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(addTodo("New item"))}>Add</button>
);
}Q19. What is the purpose of the dispatch function in Redux?
dispatch(action) is the only way to trigger a state change — it sends the action through any configured middleware and then to the reducer, which computes and returns the new state. There's no other API for mutating store state; even async logic ultimately dispatches a plain action once its work resolves.
Q20. What arguments does createStore receive in Redux?
createStore(reducer, preloadedState, enhancer);| Argument | Purpose |
|---|---|
| reducer | The root reducer function (required) |
| preloadedState | Optional initial state — e.g. for SSR hydration |
| enhancer | Optional store enhancer, e.g. applyMiddleware(...) |
Q21. Describe the subscribe function in Redux.
const unsubscribe = store.subscribe(() => {
console.log("State changed:", store.getState());
});
unsubscribe(); // stop listeningstore.subscribe(listener) registers a callback that runs after every dispatched action, whether or not the relevant slice of state actually changed — React-Redux's useSelector wraps this internally and adds its own equality check, so components only actually re-render when the specific data they selected genuinely changed.
Working with Redux Actions
Q22. How do you structure action creators in Redux?
// Classic hand-written action creator
function addTodo(text) {
return { type: "todos/add", payload: { text } };
}
// Redux Toolkit's createSlice generates these automatically
const todosSlice = createSlice({
name: "todos",
initialState: [],
reducers: {
add: (state, action) => { state.push(action.payload); },
},
});
export const { add } = todosSlice.actions; // auto-generated action creatorAn action creator is just a function returning an action object — hand-written in classic Redux, or auto-generated by createSlice in Redux Toolkit, which also lets you write reducers with direct-looking mutation (state.push(...)) safely, since it uses Immer underneath to produce an immutable update from that mutating-looking code.
Q23. What is an action type and how is it used in Redux?
The type field is the required, conventionally-string identifier telling the reducer what kind of event occurred — a switch statement (or, in Redux Toolkit's createSlice, a matching reducer key) branches on it to decide how to compute the new state. A common convention is domain/eventName (e.g. todos/add), which is exactly the format createSlice generates automatically.
Q24. Can you give an example of a synchronous action creator?
function removeTodo(id) {
return { type: "todos/remove", payload: { id } };
}Q25. How do you handle complex state logic in actions?
Keep actions themselves simple (just describing what happened) and push the complex logic — computing derived values, deciding what the new state actually looks like — into the reducer, or into a thunk if it genuinely needs to make decisions based on multiple async steps before dispatching. Actions describe intent; reducers/thunks do the actual work.
Q26. Explain the concept of action creators returning a function for asynchronous operations.
// A thunk — an action creator returning a function instead of a plain object
function fetchTodos() {
return async function (dispatch) {
dispatch({ type: "todos/loading" });
const res = await fetch("/api/todos");
const data = await res.json();
dispatch({ type: "todos/loaded", payload: data });
};
}A plain Redux store can only dispatch plain objects — the redux-thunk middleware specifically recognizes when dispatch() is given a function instead, and calls that function with (dispatch, getState) rather than passing it straight to the reducer. This is the entire mechanism that makes async action creators possible.
Reducers and State Management
Q27. How do reducers handle state changes in response to different action types?
function todosReducer(state = [], action) {
switch (action.type) {
case "todos/add": return [...state, action.payload];
case "todos/remove": return state.filter((t) => t.id !== action.payload.id);
default: return state;
}
}Q28. What is meant by a 'slice reducer'?
const todosSlice = createSlice({
name: "todos",
initialState: [],
reducers: {
add: (state, action) => { state.push(action.payload); },
remove: (state, action) => state.filter((t) => t.id !== action.payload),
},
});A "slice" is Redux Toolkit's term for one feature's reducer logic plus its automatically-generated action creators, bundled together via createSlice — the modern, less-boilerplate way of achieving what combineReducers + hand-written action creators used to require writing out separately.
Q29. Describe how to properly update the state based on an action.
// Classic (hand-written) — must be manually immutable
return { ...state, count: state.count + 1 };
// Redux Toolkit + Immer — looks mutable, produces an immutable update under the hood
state.count += 1;Q30. How would you reset the state of a Redux store?
function rootReducer(state, action) {
if (action.type === "RESET") {
state = undefined; // reducers see undefined and fall back to their initial state
}
return combinedReducer(state, action);
}Q31. Can you discuss reducer composition and its benefits?
combineReducers (and createSlice's implicit composition through configureStore) lets each slice of state be managed by its own small, focused reducer, unaware of any other slice's shape — the same divide-and-conquer benefit any modular design gives, applied specifically to state management.
Q32. What is the significance of immutability in reducer functions?
Covered above under Fundamentals — immutability is what makes reference-equality change detection (and therefore efficient re-rendering) work, and what makes time-travel debugging/replaying actions deterministic and reliable.
Q33. How do you handle initial state in Redux reducers?
function todosReducer(state = [], action) { ... }
// ^^^^ default parameter — used only if state is undefined, i.e. the very first dispatchQ34. What are common pitfalls when writing reducers?
- Mutating state directly in a plain (non-Immer) reducer instead of returning a new object/array
- Performing side effects (API calls, Date.now(), Math.random()) inside a reducer, breaking purity
- Forgetting the default case, which can return undefined and break the store
- Deeply nesting state instead of flattening/normalizing it, making updates verbose and error-prone
Redux Middleware and Side Effects
Q35. What is redux-thunk and how does it work?
Covered above — redux-thunk is middleware that lets an action creator return a function instead of a plain object; the middleware calls that function with (dispatch, getState), letting it dispatch multiple actions over time (loading, then success/failure) as an async operation progresses.
Q36. Can you explain the purpose of redux-saga and how it compares to redux-thunk?
function* fetchTodosSaga() {
yield put({ type: "todos/loading" });
const data = yield call(fetchTodosApi);
yield put({ type: "todos/loaded", payload: data });
}| redux-thunk | redux-saga | |
|---|---|---|
| Style | Plain async functions/Promises | Generator functions, declarative effects |
| Complexity handled | Simple to moderate async flows | Complex flows — cancellation, debouncing, race conditions between effects |
| Learning curve | Low — just JS functions | Higher — generators and its own effect vocabulary |
Sagas describe side effects as a sequence of declarative "effect" objects (yielded from a generator), which makes complex async orchestration (cancel this if that happens, race between two effects) much easier to express and test than the equivalent hand-written Promise chains in a thunk.
Q37. What is a 'side effect' and how do middlewares handle them in Redux?
A side effect is anything a reducer isn't allowed to do — an API call, a timer, reading the current date, logging — middleware is specifically where Redux pushes this work, intercepting actions and performing the actual effect, then dispatching plain, synchronous actions describing the effect's outcome back into the normal reducer flow.
Q38. How do you write a custom middleware for Redux?
const crashReporter = (store) => (next) => (action) => {
try {
return next(action);
} catch (err) {
console.error("Caught an exception!", err);
reportError(err);
throw err;
}
};Middleware is a curried function — store => next => action => {} — receiving the store, a next function to pass the action further down the chain, and the action itself; it can inspect/modify/delay the action, call next(action) to continue, or not call it at all to stop the action from ever reaching the reducer.
Q39. Discuss how middlewares can enhance the capabilities of a Redux store.
Beyond async handling (thunk/saga), middleware commonly adds logging (redux-logger), crash reporting, analytics tracking on specific action types, and persistence (redux-persist saving/rehydrating state to localStorage) — all cross-cutting concerns that would otherwise need to be duplicated across every component or action creator individually.
Redux and UI Integration
Q40. How do you connect a React component to a Redux store?
// Modern — hooks
const todos = useSelector((state) => state.todos);
const dispatch = useDispatch();
// Classic — the connect() higher-order component
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);Q41. What are the advantages of using React-Redux's connect function?
connect() (the pre-Hooks API) wraps a component, injecting selected state and dispatch-bound action creators as props, and automatically re-renders it only when the specifically mapped state actually changes — the same underlying optimization useSelector provides today, just via a higher-order component pattern instead of a hook.
Q42. Can you describe the mapStateToProps and mapDispatchToProps functions?
const mapStateToProps = (state) => ({ todos: state.todos });
const mapDispatchToProps = (dispatch) => ({
addTodo: (text) => dispatch({ type: "todos/add", payload: { text } }),
});
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);mapStateToProps selects which slices of store state become props on the wrapped component; mapDispatchToProps defines which dispatch-wrapped functions become props — together they're what connect() uses to decide exactly what the component receives from Redux.
Q43. Explain the Provider component from React-Redux.
import { Provider } from "react-redux";
<Provider store={store}>
<App />
</Provider>Provider makes the store available to every component in the tree beneath it via React Context, so useSelector/useDispatch (or connect()) can access it without the store being manually passed down as a prop through every intermediate component.
Q44. How do you use hooks like useDispatch and useSelector in React functional components?
function TodoList() {
const todos = useSelector((state) => state.todos);
const dispatch = useDispatch();
return (
<ul>
{todos.map((t) => (
<li key={t.id} onClick={() => dispatch(removeTodo(t.id))}>{t.text}</li>
))}
</ul>
);
}Q45. What performance considerations should be taken into account when connecting components to Redux?
- A useSelector callback re-running on every dispatched action, even unrelated ones — keep the selected slice as narrow as possible
- Returning a new object/array reference from a selector on every call forces a re-render even when the underlying data didn't meaningfully change
- Very large, deeply nested state making equality checks and updates more expensive than they need to be
Q46. How can you optimize the performance of Redux-connected components?
import { createSelector } from "@reduxjs/toolkit";
const selectCompletedTodos = createSelector(
(state) => state.todos,
(todos) => todos.filter((t) => t.completed) // only recomputed when 'todos' actually changes
);Memoized selectors (createSelector from Reselect, built into Redux Toolkit) cache their result and only recompute when their actual inputs change — critical for selectors that derive/filter/transform data, since without memoization they'd return a brand-new array reference on every single call, defeating reference-equality-based re-render optimization entirely.
Advanced Redux Patterns
Q47. What are Higher Order Reducers (HORs) and how are they used?
function undoable(reducer) {
return function (state = { past: [], present: reducer(undefined, {}) }, action) {
if (action.type === "UNDO") {
const [previous, ...rest] = state.past;
return { past: rest, present: previous };
}
const newPresent = reducer(state.present, action);
return { past: [state.present, ...state.past], present: newPresent };
};
}A higher-order reducer takes a reducer and returns a new, enhanced reducer wrapping it with extra behavior — the classic example being an undo/redo wrapper (as above), adding history tracking around any existing reducer without modifying its own logic.
Q48. Can you implement a feature toggle mechanism using Redux?
const featureFlagsSlice = createSlice({
name: "featureFlags",
initialState: { newCheckout: false },
reducers: {
setFlag: (state, action) => { state[action.payload.flag] = action.payload.enabled; },
},
});
const showNewCheckout = useSelector((state) => state.featureFlags.newCheckout);Q49. How do you normalize a state shape in Redux?
// Denormalized — nested, duplicated, hard to update one item
{ todos: [{ id: 1, text: "...", author: { id: 5, name: "Ada" } }] }
// Normalized — flat, keyed by ID, single source of truth per entity
{
todos: { byId: { 1: { id: 1, text: "...", authorId: 5 } }, allIds: [1] },
authors: { byId: { 5: { id: 5, name: "Ada" } }, allIds: [5] },
}Q50. What is a Redux 'slice' and how is it used?
Covered above under Reducers — a slice is the Redux Toolkit unit combining one feature's reducer(s), initial state, and auto-generated actions via createSlice, and configureStore combines multiple slices into the full store.
Q51. Explain the 'Ducks' pattern in Redux.
The Ducks pattern co-locates a feature's action types, action creators, and reducer in a single file (rather than spreading them across separate actions/ and reducers/ folders), exported together — a file-organization convention that Redux Toolkit's createSlice essentially formalizes and builds directly into the framework.
Redux State Normalization
Q52. Why is state normalization recommended in Redux?
Normalized state avoids data duplication (the same entity nested in multiple places, which can drift out of sync when updated in only one), makes updating a single entity a simple, flat operation rather than a deep, error-prone nested update, and makes relational-style lookups ("give me author #5") an O(1) key access instead of a search through nested arrays.
Q53. How do you structure a normalized state?
{
entities: {
todos: { 1: { id: 1, text: "Buy milk", authorId: 5 } },
authors: { 5: { id: 5, name: "Ada" } },
},
ids: { todos: [1], authors: [5] },
}The common convention (also what Redux Toolkit's createEntityAdapter generates) keeps each entity type in its own byId map plus a parallel allIds array for ordering — updates to one entity touch exactly one key, and relationships between entities are expressed as ID references, not nested duplication.
Q54. What are the benefits of state denormalization?
Denormalization (reassembling normalized data into a nested shape at read time, via a selector) is what UI components actually want to consume — a TodoItem component shouldn't have to separately look up its author by ID; a selector can join todos and authors back together into the nested shape a component naturally renders.
Q55. Can you explain how to manage entity relations in a normalized Redux state?
const selectTodoWithAuthor = (state, todoId) => {
const todo = state.entities.todos[todoId];
return { ...todo, author: state.entities.authors[todo.authorId] };
};Relations are stored as plain ID references (authorId, not a nested author object) at the state level, and joined back together only where needed — typically inside a selector, keeping the store itself relational/normalized while components still receive a convenient, denormalized shape.
Redux Ecosystem and Utilities
Q56. What is Immutable.js and how can it be used with Redux?
Immutable.js provides persistent, structurally-shared immutable data structures (List, Map) that enforce immutability at the data-structure level, rather than relying on developer discipline (spread syntax) to avoid mutation. It's largely fallen out of favor since Redux Toolkit's Immer-based approach achieves the same safety with a much friendlier, plain-JS-object-looking API and no separate data-structure library to learn.
Q57. Discuss the Redux DevTools extension and its features.
Redux DevTools shows every dispatched action, the resulting state diff, and lets you time-travel — jump back to any previous state, or replay actions from scratch — invaluable for debugging exactly what sequence of actions produced a given (possibly buggy) state, something plain console.log debugging can't easily replicate.
Q58. How do reselect and memoized selectors work in Redux?
Covered above under performance — Reselect's createSelector caches a selector's result and only recomputes when its declared input selectors' outputs actually change, avoiding both wasted recomputation and the new-reference-every-call problem that would otherwise defeat useSelector's re-render optimization for any derived/computed data.
Q59. What are common utilities and libraries used in conjunction with Redux?
| Library | Purpose |
|---|---|
| Redux Toolkit | The official, recommended way to write Redux logic today |
| Reselect | Memoized selectors (built into RTK already) |
| redux-persist | Persisting store state to localStorage/AsyncStorage across reloads |
| redux-saga / redux-thunk | Handling async side effects |
Q60. Describe the significance of the Redux Toolkit and how it helps Redux development.
Redux Toolkit is the officially recommended way to write Redux today — createSlice generates action creators and a reducer together, Immer lets you write reducers with mutating-looking syntax that's actually immutable underneath, configureStore sets up good defaults (DevTools, thunk middleware) automatically, and createAsyncThunk standardizes the loading/success/error pattern for async actions. It exists specifically to eliminate the boilerplate that made "classic" hand-written Redux tedious.
Testing Redux Applications
Q61. How do you test Redux reducers?
test("adds a todo", () => {
const state = todosReducer([], { type: "todos/add", payload: { text: "Buy milk" } });
expect(state).toEqual([{ text: "Buy milk" }]);
});Because reducers are pure functions, testing one is just calling it with a given (state, action) pair and asserting on the result — no mocking, no rendering, no async handling needed for the reducer logic itself.
Q62. What is the recommended approach to test Redux actions?
test("addTodo creates the right action", () => {
expect(addTodo("Buy milk")).toEqual({ type: "todos/add", payload: { text: "Buy milk" } });
});Q63. How can you test a Redux-connected React component?
import { render, screen } from "@testing-library/react";
import { Provider } from "react-redux";
render(
<Provider store={testStore}>
<TodoList />
</Provider>
);
expect(screen.getByText("Buy milk")).toBeInTheDocument();Wrap the component under test in a Provider with a real (or purpose-built test) store, then interact with and assert on it exactly like any other React Testing Library test — treating Redux as an implementation detail the test doesn't need to mock out separately.
Q64. Describe the process of testing asynchronous action creators.
test("fetchTodos dispatches loading then loaded", async () => {
const dispatch = jest.fn();
await fetchTodos()(dispatch, () => ({}));
expect(dispatch).toHaveBeenCalledWith({ type: "todos/loading" });
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({ type: "todos/loaded" })
);
});Q65. What tools can you use to facilitate testing in Redux applications?
| Tool | Use |
|---|---|
| Jest / Vitest | Test runner and assertions |
| React Testing Library | Rendering and interacting with connected components |
| redux-mock-store | A lightweight fake store for testing action-dispatching logic in isolation |
Redux with Other Frameworks and Libraries
Q66. How do you integrate Redux with a framework other than React, like Angular or Vue?
Redux itself is completely framework-agnostic — it's just a JS library with getState/dispatch/subscribe. Vue and Angular each have their own more idiomatic state-management libraries (Pinia/Vuex for Vue, NgRx for Angular, itself heavily Redux-inspired) that most projects reach for instead, but plain Redux can be wired into any framework by subscribing to store changes and manually updating that framework's own reactive state.
Q67. Can Redux be used with server-side rendering frameworks like Next.js?
// Server: render with a pre-populated store
const store = configureStore({ reducer, preloadedState: initialData });
// Client: hydrate the same initial state into a fresh store on mount
const store = configureStore({ reducer, preloadedState: window.__PRELOADED_STATE__ });Yes — create a store per request on the server, populate it with the data needed for that render, serialize that state into the HTML response, and rehydrate a client-side store from that same initial state on load, so the client picks up exactly where the server left off without an extra round trip.
Q68. Discuss the use of Redux with React Native.
Identical to web React-Redux usage — Provider, useSelector, useDispatch all work exactly the same, since Redux itself has no dependency on the DOM. redux-persist commonly swaps its storage engine to AsyncStorage instead of localStorage for React Native specifically.
Q69. What adaptations are needed to use Redux outside of a UI environment (e.g., in a Node.js backend)?
None, fundamentally — Redux's core (createStore/dispatch/subscribe) has zero UI dependency, so it can manage state in a Node.js process just as well; you'd simply skip react-redux entirely and interact with store.getState()/dispatch() directly, though this is genuinely rare in practice since server-side code rarely needs Redux's specific benefits over a plain in-memory object or a real database.
Scaling Redux for Large Applications
Q70. How do you organize your files and folders in a large Redux application?
The Ducks/feature-folder pattern (each feature's slice, components, and selectors colocated in one folder, e.g. features/todos/) is the modern standard, replacing the older pattern of separate top-level actions/ and reducers/ folders that scattered one feature's logic across multiple distant files.
Q71. What strategies can be used to scale Redux actions and reducers?
- One slice per feature/domain via createSlice, combined through configureStore
- Shared, reusable action types/utilities for genuinely cross-cutting concerns
- Lazy-loading a feature's slice only when that feature is actually used (code-split reducers), for very large apps
Q72. Can you explain the concept of 'domain-driven design' in the context of a Redux state tree?
Structuring the state tree (and the slices that manage it) around business domains (orders, inventory, users) rather than around technical/UI concerns — each domain's slice owns its own state shape and logic independently, mirroring how the business itself is organized, similar in spirit to bounded contexts in domain-driven design generally.
Q73. How do feature modules and code splitting affect Redux state management?
// A feature's reducer is added to the store dynamically once its code loads
store.replaceReducer(combineReducers({ ...existingReducers, newFeature: newFeatureReducer }));For large apps splitting code by route/feature, the corresponding Redux slice can be added to the store dynamically (via store.replaceReducer) only once that feature's code is actually loaded — avoiding bundling every feature's reducer logic into the initial JS payload regardless of whether the user ever visits that feature.
Q74. Discuss strategies for managing complex application state with Redux.
- Normalize entity data (covered earlier) rather than deeply nesting it
- Keep UI-only, ephemeral state (a modal's open/closed status) in local component state, not Redux — not everything belongs in the global store
- Use createEntityAdapter for consistent, boilerplate-free CRUD-style entity slices
Redux Performance Optimization
Q75. How do you prevent unnecessary renders in Redux-connected components?
- Select the narrowest possible slice of state in useSelector, not a large object that changes for unrelated reasons
- Use memoized selectors (createSelector) for any derived/computed data
- Split a large component into smaller ones each selecting only what they individually need, rather than one component subscribing broadly
Q76. What techniques are available to reduce the size of the Redux store?
Normalize data to avoid duplication, don't store data that can be trivially derived (compute it in a selector instead), and keep genuinely ephemeral/local UI state (form input values, a dropdown's open state) out of Redux entirely rather than growing the global store with things only one component cares about.
Q77. How do you optimize Redux action dispatch and payload size?
Keep action payloads minimal — dispatch an ID and let the reducer/selector look up the rest from existing state, rather than shipping an entire large object through every action unnecessarily; batch multiple related state changes into one action where reasonable, instead of several separate dispatches causing several separate re-render cycles.
Q78. What are best practices for ensuring a scalable Redux state management with large datasets or frequent updates?
- Normalize large collections (createEntityAdapter handles this well out of the box)
- Paginate/virtualize rather than holding every record in state simultaneously
- Debounce or batch high-frequency updates (e.g. from a WebSocket stream) instead of dispatching an action per individual message
Redux and Forms
Q79. How do you manage form state with Redux?
const formSlice = createSlice({
name: "loginForm",
initialState: { email: "", password: "" },
reducers: {
setField: (state, action) => { state[action.payload.field] = action.payload.value; },
},
});Q80. Discuss the use of libraries like Redux Form or React Final Form with Redux.
Redux Form (largely deprecated now) put every keystroke of every form field into the Redux store, dispatching an action per input change — this turned out to be a real performance problem for large forms and is now generally discouraged; React Final Form and modern alternatives (React Hook Form) keep form state local to the form itself by default, which performs far better.
Q81. What are the trade-offs of keeping form state in the Redux store?
| Keeping form state in Redux | Keeping it local (useState/React Hook Form) | |
|---|---|---|
| Pros | Inspectable in DevTools, easy to persist/restore across navigation | Much better performance — no store dispatch per keystroke |
| Cons | An action dispatched per keystroke, unnecessary global-store noise | Not directly visible in Redux DevTools |
The modern consensus: keep form state local unless there's a specific reason it needs to be globally accessible (a multi-step wizard whose state must survive navigating away and back) — most forms don't need that, and the performance cost of routing every keystroke through Redux usually isn't worth it.
Redux and Authentication
Q82. How do you manage user authentication and session state in Redux?
const authSlice = createSlice({
name: "auth",
initialState: { user: null, token: null },
reducers: {
loginSuccess: (state, action) => {
state.user = action.payload.user;
state.token = action.payload.token;
},
logout: (state) => { state.user = null; state.token = null; },
},
});Q83. What considerations should be made for security when storing tokens or sensitive information in Redux?
Redux state lives in plain JavaScript memory, fully visible via Redux DevTools and any XSS-injected script — never store anything more sensitive than an access token there, never a raw password, and be aware that persisting auth state to localStorage (via redux-persist) has the same XSS-exposure trade-off as any other localStorage-based token storage, covered in more depth in the Web Security interview post.
Q84. How do you manage login and logout workflows with Redux actions and reducers?
export const login = createAsyncThunk("auth/login", async (credentials) => {
const res = await api.post("/login", credentials);
return res.data; // { user, token }
});
// slice's extraReducers handle pending/fulfilled/rejected automatically via createAsyncThunkMigrating to Redux and State Hydration
Q85. How do you approach migrating a legacy application state management to Redux?
- Migrate one feature/domain at a time, not the whole app in one pass
- Start with genuinely shared, cross-component state — the state that most justifies Redux in the first place
- Keep legacy and Redux-managed state coexisting temporarily during the transition, rather than forcing a risky big-bang cutover
Q86. Can you explain the concept of 'state hydration' in the context of SSR and Redux?
Covered above under SSR/Next.js — hydration is taking the state a server already computed (serialized into the initial HTML response) and using it as the client-side store's starting state, so the client doesn't have to re-fetch and recompute everything the server already did.
Q87. What challenges might arise when integrating Redux into an existing application and how would you address them?
State shape conflicts between the legacy state model and Redux's expectations, resistance to the extra ceremony from a team used to simpler local state, and a genuine risk of over-applying Redux to state that didn't actually need to be global — address these by migrating incrementally, and being disciplined about which state genuinely belongs in Redux versus staying local.
Redux Future and Community
Q88. Describe the current trends and community practices around Redux.
Redux Toolkit is now the unambiguous default, official way to write Redux — plain hand-written reducers/action creators without RTK are considered legacy style, even though understanding them still matters for reading older codebases and for genuinely grasping what RTK is abstracting away.
Q89. How has Redux evolved to fit into the modern React ecosystem with hooks?
useSelector/useDispatch replaced connect()'s higher-order-component pattern with a much more direct, less boilerplate-heavy way to interact with the store from function components — fitting naturally into the hooks-first style the rest of modern React has converged on.
Q90. What do you believe is the future of Redux given the context of emerging state management libraries?
Lighter alternatives (Zustand, Jotai, React Query/TanStack Query for server-state specifically) have taken real mindshare for apps that don't need Redux's full ceremony — the honest, current industry read is that Redux (via Redux Toolkit) remains the right choice for large, complex apps with genuinely intricate shared client state, while React Query has become the clear preferred tool specifically for server-fetched data, a job Redux was never really the best fit for in the first place.
Advanced State Modeling in Redux
Q91. How do you model complex state dependencies and computations in Redux?
Keep the store itself as flat/normalized raw data, and compute derived/dependent values through memoized selectors rather than storing computed values redundantly in the state tree itself — this avoids the classic problem of a derived value silently going stale because the code that should have recomputed it after a related change was forgotten.
Q92. Discuss your approach to type checking and ensuring state consistency.
interface TodosState { items: Todo[]; status: "idle" | "loading" | "error"; }
const todosSlice = createSlice({
name: "todos",
initialState: { items: [], status: "idle" } as TodosState,
reducers: { /* ... */ },
});TypeScript with Redux Toolkit's built-in type inference (PayloadAction<T>, typed state/dispatch hooks) catches a large class of state-shape mistakes at compile time — a mismatched action payload, an invalid state transition — that plain JS Redux would only surface at runtime, if at all.
Q93. How do you tackle transactional updates in Redux?
A single dispatched action, handled by a single reducer call, is already atomic — multiple pieces of state can be updated together in one reducer's logic (or across multiple slices' extraReducers responding to the same action type) so they change together in one render cycle, rather than as several separate dispatches that could leave state briefly inconsistent between them.
Q94. What strategies would you use for undo/redo functionality in a Redux application?
// redux-undo wraps a reducer, adding past/present/future automatically
import undoable from "redux-undo";
const undoableTodos = undoable(todosReducer);The higher-order reducer pattern covered earlier (redux-undo is a ready-made library implementing exactly this) — wrapping a reducer to track past/present/future state, with UNDO/REDO actions navigating that history without the underlying feature reducer needing any undo-specific logic of its own.
Redux Interoperability
Q95. How does Redux interact with WebSockets or external data sources?
socket.on("todo:created", (todo) => {
store.dispatch({ type: "todos/received", payload: todo });
});A WebSocket (or any external push source) is just another origin of dispatched actions — a message handler translates the incoming event into a plain Redux action and dispatches it, and from the reducer's perspective it's indistinguishable from an action triggered by a UI click.
Q96. Discuss the integration of GraphQL in a Redux-managed application.
Historically, GraphQL responses were dispatched into Redux like any other fetched data — today, most teams instead use Apollo Client or RTK Query (Redux Toolkit's own data-fetching addition) for GraphQL/REST data specifically, since those libraries handle caching, refetching, and loading states purpose-built for server data far better than hand-rolled Redux thunks ever did.
Q97. How would you manage local vs. server state in a Redux application?
| Client/UI state | Server state | |
|---|---|---|
| Examples | Modal open/closed, form input, selected tab | Fetched API data, its loading/error/cache status |
| Best managed by | Local component state, or a Redux slice for genuinely shared UI state | RTK Query / React Query — purpose-built for caching and refetching |
Server state has fundamentally different needs (staleness, caching, refetching, deduplication) than client state does — the modern consensus is treating them as genuinely separate concerns rather than forcing all fetched API data through the same thunk-and-reducer pattern as pure client state.
Redux API Requests and Caching
Q98. How do you structure API requests in Redux actions?
export const fetchTodos = createAsyncThunk("todos/fetch", async () => {
const res = await fetch("/api/todos");
if (!res.ok) throw new Error("Failed to fetch");
return res.json();
});
// createSlice's extraReducers respond to fetchTodos.pending/.fulfilled/.rejectedQ99. Discuss methods for caching API responses in the Redux store.
Hand-rolled caching (storing a timestamp alongside fetched data, checking staleness before refetching) works but is exactly the kind of boilerplate RTK Query was built to eliminate — it handles cache invalidation, deduplication of in-flight requests, and automatic refetch-on-mount policies out of the box.
Q100. What patterns or libraries do you use to manage RESTful or GraphQL API interactions with Redux?
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
endpoints: (builder) => ({
getTodos: builder.query({ query: () => "todos" }),
}),
});
export const { useGetTodosQuery } = api; // auto-generated hook, with caching built inRTK Query (part of Redux Toolkit) is the current recommended approach for API interaction specifically — define endpoints declaratively, and it generates hooks handling caching, loading/error states, and refetch triggers automatically, integrated directly into the same store as the rest of your app's Redux state rather than being a separate parallel system.
Test yourself
A quick mock test covering the ground above — the rapid-fire round that often opens a Redux interview before the deeper follow-ups.
Question 1 of 8
Score: 0 / 0
1. What are the three core principles Redux is built on?