95 Redux Interview Questions and Answers (2026)

As apps scale and state gets messy, Redux is still what teams reach for—and interviewers know it. The bar has moved: they don't want buzzwords, they want to see you reason through actions, reducers, and async flows on the spot. Walk in shaky and it shows fast.
This guide gives you 95 questions with concise, interview-ready answers and code where it counts. It's worked Junior to Mid to Senior, so you build from the fundamentals up to the deep tradeoffs. Work through it and you'll speak Redux like you've shipped it.
Q1.What are the three fundamental principles of Redux, and how do they contribute to predictable state management?
Redux rests on three principles: a single source of truth, read-only state, and changes via pure functions. Together they make state changes explicit, traceable, and reproducible.
Single source of truth:
The whole app state lives in one object tree inside a single store.
Makes debugging, persistence, and hydration simple: you inspect one place.
State is read-only:
You never mutate state directly; the only way to change it is to dispatch an action.
Every change is described by a plain object, so intent is explicit and loggable.
Changes are made with pure reducers:
A reducer is (state, action) => newState, with no side effects and no mutation.
Same inputs always produce the same output, which enables testing, replay, and time travel.
Predictability comes from the combination: state only moves through one funnel (dispatch to pure reducer), so any state can be reconstructed from the initial state plus the ordered list of actions.
Q2.What is the role of the 'Store' in Redux, and what are its primary methods?
The store is the object that holds the entire application state tree and ties the pieces together: it dispatches actions, runs the reducer, and lets the UI subscribe to changes. There is exactly one per app.
Responsibilities: Holds current state, allows access to it, and coordinates updates through the reducer.
getState(): Returns the current state tree.
dispatch(action): The only way to trigger a state change; sends the action through middleware and the reducer.
subscribe(listener): Registers a callback that runs after every dispatch; returns an unsubscribe function.
replaceReducer(nextReducer): Swaps the reducer at runtime, used for code splitting and hot reloading.
Q3.What is the difference between "State" and "Actions" in the Redux pattern?
State is the current data (the "what is"); an action is a plain object describing an event that requests a change (the "what happened"). Actions are inputs; state is the result the reducer produces from them.
State:
The single, read-only object tree held by the store representing everything the app currently knows.
Changed only by producing a new object from a reducer, never mutated in place.
Action:
A plain object with a type field (and usually a payload) describing an intent or event.
It only declares that something happened; it contains no logic and changes nothing by itself.
How they relate: The reducer combines them: newState = reducer(state, action).
Q4.What is Redux and what specific problems does it solve in a modern React application?
React application?Redux is a predictable state container for JavaScript apps: it centralizes application state in one store and enforces that all changes flow through dispatched actions and pure reducers. In React, it solves the problems of scattered shared state, prop drilling, and hard-to-trace updates.
Centralizes shared state: State many components need (auth, cart, cache) lives in one store instead of being duplicated across the tree.
Eliminates prop drilling: Any component can read or update state via the store rather than passing props through many intermediate layers.
Makes updates predictable and traceable: All changes go through dispatch and pure reducers, so every mutation is explicit and reproducible.
Powerful tooling: Enables logging, time-travel debugging, and state persistence thanks to serializable actions and immutable state.
When it is overkill: For local or simple state, useState / useContext may suffice; reach for Redux (ideally Redux Toolkit) when shared, complex, or frequently updated state grows unmanageable.
Q5.What is the role of the store? How does it differ from a standard JavaScript object?
JavaScript object?The store is the object that holds the entire Redux state tree and exposes a small, controlled API for reading and updating it: unlike a plain object, you can never mutate it directly.
Responsibilities of the store:
Holds application state, accessed via getState().
Allows updates only by dispatching actions through dispatch(action).
Registers listeners via subscribe(listener) that run on every state change.
How it differs from a plain JS object:
A plain object can be reassigned or mutated anywhere; store state is read-only and only the reducer produces the next state.
Updates are predictable: every change flows through dispatch then the reducer, which returns a new state.
It adds change notification (subscription) that a raw object has no concept of.
Created by configureStore() (Redux Toolkit) or the legacy createStore().
Q6.What is the role of the Redux Store? Can an application have more than one store, and why is a single source of truth preferred?
Store? Can an application have more than one store, and why is a single source of truth preferred?The Redux store centralizes all app state in one tree with one dispatch pipeline. You technically can create multiple stores, but a single store (single source of truth) is strongly preferred because it makes state predictable, debuggable, and easy to reason about.
Role of the store: One tree accessed with getState(), updated only via dispatch(), observed with subscribe().
Can you have more than one?:
Yes, it is possible, but it is an anti-pattern in normal apps.
Multiple stores fragment state, complicate cross-store dependencies, and break tooling that assumes one store.
Why single source of truth:
Predictability: all state changes go through one pipeline.
Debuggability: time-travel and Redux DevTools work on one consistent snapshot.
Serialization: easy to persist, hydrate, or log the whole state at once.
Split concerns with combined reducers/slices under one store, not multiple stores.
Q7.What are 'Action Creators' and are they strictly necessary in Redux?
An action creator is simply a function that returns an action object. They are not strictly required (you can dispatch plain object literals), but they are a best practice because they centralize action shape and reduce repetition and errors.
What they are: A factory: call it with arguments and it returns { type, payload }.
Why use them:
Avoid hand-writing the same object shape in many places.
Single place to change the action's structure or add logic.
Easier to test and to reference consistently.
Strictly necessary?:
No: dispatch({ type: 'todos/added', payload }) works fine.
In practice Redux Toolkit generates them for you: createSlice exposes ready-made action creators per reducer.
Q8.What is an action in Redux, and what shape or structure does a typical action object have?
An action is a plain JavaScript object that describes what happened in the app. It is the only way to send data into the store, and it must have a type field identifying the event.
Required field: type: a string that names the event, e.g. 'todos/added'.
Common optional fields:
payload: the data carried by the action.
meta: extra info not part of the payload.
error: indicates the action represents a failure.
Key traits:
Should be a plain, serializable object (no functions/promises for normal actions).
Describes intent ("what happened"), not how the state changes: that is the reducer's job.
Q9.What does calling dispatch actually return, and why can that be useful?
dispatch actually return, and why can that be useful?By default store.dispatch(action) returns the very action object you passed in, but middleware can change that: with redux-thunk dispatching a thunk returns whatever that thunk function returns.
Plain dispatch returns the action: The base dispatch runs the action through reducers and hands the same action back, so you can inspect or chain on it.
Middleware can override the return value: With redux-thunk, dispatch(someThunk()) returns the thunk's return value, commonly a Promise.
Why it's useful:
RTK's createAsyncThunk returns a Promise you can await, and calling .unwrap() on it lets a component handle success/failure directly.
This lets UI code sequence work after a dispatch completes (navigate, show a toast) without subscribing to the store.
Q10.What is combineReducers, and how does reducer composition let you split a large reducer into smaller ones?
combineReducers, and how does reducer composition let you split a large reducer into smaller ones?combineReducers is a helper that merges several slice reducers into one root reducer, each owning a top-level key of state. This is reducer composition: a big reducer becomes many small, focused ones.
Each reducer owns one slice: The keys you pass become the state shape: combineReducers({ users, posts }) yields state.users and state.posts.
How it dispatches: The root reducer forwards every action to all slice reducers; each one only responds to actions it cares about and returns its own new sub-state.
Isolation is the benefit:
A slice reducer only sees and returns its own slice, keeping logic small and testable.
Cross-slice logic needs a different approach (a reducer above the combined ones, or extraReducers).
RTK: configureStore calls combineReducers for you when you pass a reducer object.
Q11.What happens when you dispatch an action that no reducer handles, and why do reducers return the existing state in their default case?
Nothing breaks: every reducer runs but none matches the action type, so each returns its current state unchanged, and the store ends up with the same state reference. Returning existing state in the default case is what makes this safe.
Why the default case returns state:
Every dispatched action is sent to every reducer, so a reducer constantly sees actions meant for others; it must return its own state untouched.
Returning undefined instead would wipe that slice, so the default is mandatory.
Same reference means no re-render: Because state is returned as-is, prev === next holds and subscribers are not re-rendered needlessly.
Initialization relies on this too: Redux's internal @@INIT action hits the default case, which is where the initialState gets established.
In RTK: createSlice handles the default automatically, so unhandled actions leave the slice alone without you writing the case.
Q12.What is getState, and when would a thunk need to read the current state before dispatching?
getState, and when would a thunk need to read the current state before dispatching?getState is the store method that returns the entire current state tree. Inside a thunk it's the second argument, letting async logic read what's already in the store before deciding what (or whether) to dispatch.
Thunk signature: A thunk is (dispatch, getState) => { ... }; call getState() to get the latest snapshot.
When a thunk needs it:
Conditional dispatch: skip a fetch if the data is already cached or a request is in flight.
Guarding on auth/permissions: read a token or user role before dispatching.
Building a request from existing state (filters, pagination, current selection).
Note: it reads the freshest state each time you call it, which matters in async flows where state may have changed between awaits.
Q13.What is applyMiddleware, and what role does it play when creating a store?
applyMiddleware, and what role does it play when creating a store?applyMiddleware is a store enhancer that wraps the store's dispatch so that dispatched actions flow through a chain of middleware before reaching the reducer. It's how you add capabilities like async handling and logging.
What it does:
Composes middleware into a pipeline; each middleware can inspect, delay, modify, or stop an action, or dispatch new ones.
Order matters: actions pass through middleware left to right.
Role at store creation:
Passed to createStore as the enhancer: createStore(reducer, applyMiddleware(thunk, logger)).
Middleware has the signature store => next => action => {}, giving access to dispatch and getState.
With Redux Toolkit: configureStore applies middleware for you; you customize via the middleware option instead of calling applyMiddleware directly.
Q14.How do you set up and connect the Redux DevTools to your store?
You connect the browser's Redux DevTools Extension by wiring it into the store as an enhancer. With Redux Toolkit this is automatic; with plain Redux you add it manually via a compose helper.
With Redux Toolkit (recommended):
configureStore enables DevTools automatically in development and disables it in production.
Customize with the devTools option (boolean or a config object with name, action sanitizers, etc.).
With plain createStore: Use composeWithDevTools from @redux-devtools/extension, or read window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__.
What you get:
Action log, state diffs, time-travel debugging, and action replay.
Requires the browser extension installed; disable in production to avoid leaking state.
Q15.What is the role of the Provider component in a React-Redux application, and how does it make the store accessible to deeply nested components?
Provider component in a React-Redux application, and how does it make the store accessible to deeply nested components?The <Provider> component wraps your app and places the Redux store into React context, so any descendant can access it without props being threaded through every level.
It puts the store on context:
You pass the store once: <Provider store={store}>.
React-Redux uses its own internal context (ReactReduxContext), not raw React state.
Deeply nested access without prop drilling: Hooks like useSelector and useDispatch (and connect) read the store from that context regardless of depth.
Efficient subscriptions: It does not re-render children on every state change via context; instead components subscribe to the store directly and re-render selectively based on their selectors.
Must be near the root: Any component using Redux hooks must be inside the <Provider> tree, or it throws.
Q16.Why do we need Thunks or Sagas for asynchronous logic? Why can't we just put an async call in a standard action creator?
Thunks or Sagas for asynchronous logic? Why can't we just put an async call in a standard action creator?Because reducers and dispatch must be synchronous and pure: an action must be a plain object available immediately, so any async work needs a middleware layer to delay dispatching the real result.
Actions must be plain objects: By default dispatch expects an object; returning a Promise or async result gives the reducer nothing usable synchronously.
Reducers must be pure and synchronous: They can't perform side effects or wait; they only compute new state from the current state plus an action.
Middleware bridges the gap: Thunk/Saga intercept dispatched functions or effects, run the async work, then dispatch plain result actions when the data resolves.
So the async call itself is fine, but it belongs in middleware that dispatches pending/success/failure actions, not inside the reducer.
Q17.Explain the concept of a "Slice" in Redux Toolkit and how createSlice combines actions and reducers.
createSlice combines actions and reducers.A slice is a self-contained bundle of the reducer logic and actions for a single feature of your state. createSlice takes a name, initial state, and a reducers object, then auto-generates the action creators and action types to match each reducer function.
What you provide:
name: prefix for generated action types (e.g. 'counter/increment').
initialState and a reducers map of case reducers.
What it generates: slice.reducer for the store, and slice.actions with one action creator per reducer key.
Immer under the hood: You write "mutating" code (state.value++) and Immer produces an immutable update.
Q18.Why is Redux Toolkit now the official recommendation? What specific pain points or boilerplate of classic Redux does it solve?
Redux Toolkit (RTK) is the official recommendation because it removes the ceremony that made classic Redux verbose and error-prone, giving sensible defaults while keeping the same core principles.
Less boilerplate: createSlice replaces hand-written action types, action creators, and switch-statement reducers.
Immutable updates made easy: Immer lets you write direct mutations, avoiding fragile spread logic and accidental state mutation bugs.
Store setup with good defaults: configureStore wires up redux-thunk, DevTools, and serializability/immutability checks automatically.
Async built in: createAsyncThunk standardizes pending/fulfilled/rejected handling; RTK Query eliminates most data-fetching code.
Fewer footguns: Built-in dev checks catch mutations and non-serializable values early.
Q19.What are selectors, and why is it a best practice to use them instead of accessing the state directly in components?
Selectors are functions that take the Redux state and return a specific slice or derived piece of it. Using them instead of reaching into state.a.b.c directly in components centralizes the knowledge of state shape, so refactors and derivations happen in one place.
Encapsulation of state shape:
Components ask selectActiveUser(state) and don't know or care how the state is nested.
If the shape changes, you update the selector, not every component.
Reusability and testability: The same selector can be shared across components and unit-tested as a pure function.
A home for derived data: Filtering/sorting logic lives in selectors (ideally memoized) rather than being duplicated in components.
Works cleanly with useSelector: A well-scoped selector returns only what the component needs, minimizing re-renders.
Q20.Explain the lifecycle of an action in Redux. What happens from the moment dispatch is called until the UI updates?
dispatch is called until the UI updates?When dispatch(action) is called, the action flows through middleware, then the root reducer computes a new state, subscribers fire, and connected components re-render if their selected data changed.
Dispatch is called: You call store.dispatch(action) with a plain object like { type, payload }.
Middleware runs: The action passes through the middleware chain (redux-thunk, logging, etc.); async logic can dispatch further actions here.
The root reducer runs: The store calls the reducer with the current state and the action, producing a new state object (no mutation).
The store updates and notifies: The new state replaces the old, then every function registered via store.subscribe() is called.
UI re-renders selectively: React-Redux's useSelector reads new state, compares its selected slice, and re-renders only if that slice changed by reference.
Q21.Redux is often described as a descendant of the Flux architecture. What are the primary differences between the two?
Redux keeps Flux's core idea (unidirectional data flow driven by actions) but simplifies it: a single store instead of many, and pure reducer functions instead of stateful dispatcher-registered stores.
Number of stores: Flux has multiple stores; Redux has a single store holding one state tree.
The dispatcher: Flux uses a central Dispatcher object that stores register with; Redux has no separate dispatcher: dispatch is a store method.
How state changes: Flux stores contain logic and mutate their own state; Redux uses pure reducers that return new state and hold no state themselves.
Composition: Redux composes reducers with combineReducers, replacing Flux's independent store objects.
What they share: Both enforce one-directional flow: action to state to view, never the view mutating state directly.
Q22.What is a Flux Standard Action (FSA), and what fields does it standardize?
A Flux Standard Action (FSA) is a community convention for a consistent action shape, so all actions look the same and tooling/middleware can rely on predictable fields.
Fields it standardizes:
type: required identifier of the action.
payload: the main data; by convention it holds the value or, when error is true, an Error object.
error: optional boolean, true if the action represents a failure.
meta: optional extra information not belonging in the payload.
Rules:
An action must have type and may have payload, error, meta and nothing else.
Puts all data under payload so consumers know where to look.
Redux Toolkit's createAction produces FSA-compliant actions by default.
Q23.What naming conventions do you follow for action types, and why does the 'domain/eventName' convention matter?
A common convention is naming action types as 'domain/eventName', where the domain is the feature/slice and eventName is what happened. It keeps types unique, readable, and organized by feature.
The pattern:
domain: the slice or feature, e.g. todos, auth.
eventName: a past-tense description of the event, e.g. 'todos/todoAdded'.
Why it matters:
Uniqueness: the domain prefix prevents collisions between slices.
Readability: DevTools logs read like an event history grouped by feature.
Intent: past-tense event names reinforce that actions describe what happened, not commands.
Redux Toolkit reinforces this: createSlice auto-generates types as sliceName/reducerName.
Q24.What is createAction in Redux Toolkit, and what does the generated action creator return?
createAction in Redux Toolkit, and what does the generated action creator return?createAction is a Redux Toolkit helper that generates an action creator from a type string. Calling the generated creator returns an FSA-shaped action object with that type and the argument placed under payload.
What you get:
A callable action creator plus a toString() and .type property equal to the type string.
Because toString() returns the type, it can be used directly as a key in extraReducers builder cases.
What it returns when called:
increment(5) produces { type: 'increment', payload: 5 }.
Accepts an optional prepare callback to customize how the payload is built.
Q25.What is the 'prepare' callback in createSlice, and when would you use it to customize an action's payload?
createSlice, and when would you use it to customize an action's payload?The prepare callback is a function you attach to a reducer in createSlice (or createAction) to customize the action object before it reaches the reducer. It lets you shape the payload and add meta or error fields.
How it works:
You provide an object with reducer and prepare functions.
prepare receives the caller's arguments and must return an object like { payload, meta?, error? }.
When to use it:
Generate values inside the action, e.g. an id via nanoid() or a timestamp.
Combine multiple call arguments into a single structured payload.
Add meta without polluting the payload.
Q26.Why is it mandatory for Redux reducers to be pure functions? What are the specific consequences of introducing side effects or mutations inside a reducer?
Reducers must be pure (same input always yields the same output, with no side effects) because Redux relies on predictability, reference-based change detection, and replayability. Mutations or side effects break all three.
Purity means deterministic and side-effect free: Given the same state and action, it returns the same new state and does nothing else (no API calls, no Date.now(), no mutation).
Consequence of mutating state:
Redux compares references (prev === next); mutating in place keeps the same reference, so useSelector / connected components don't re-render.
It also corrupts time-travel debugging, since past states share the mutated object.
Consequence of side effects:
Non-determinism makes bugs unreproducible and breaks replaying actions from a log.
Effects belong in middleware (thunks, sagas) or createAsyncThunk, not reducers.
Q27.How does Immer work within Redux Toolkit, and why does it allow us to write "mutative" code safely?
Immer work within Redux Toolkit, and why does it allow us to write "mutative" code safely?Immer lets you write code that looks like you're mutating state, but under the hood it produces a new immutable copy. RTK's createSlice and createReducer wrap every reducer in Immer automatically.
It uses a draft proxy: Immer hands your reducer a draft, a Proxy that records every write you make.
It produces the next state structurally: From the recorded changes Immer builds a new state, copying only the parts that actually changed and sharing untouched branches (structural sharing).
Two valid reducer styles:
Either mutate the draft and return nothing, OR return a brand new value.
Don't do both in one reducer (mutate the draft and also return it).
Why it's safe: You never touch the real state; the "mutation" is on a throwaway draft, so the guarantee of immutability holds.
Q28.Why is immutability a core requirement in Redux? How does the library detect changes to the state tree?
Immutability is core because Redux detects change by comparing object references, not by deep-comparing values. Each update must return a new object so a fast === check reveals what changed.
Reference equality drives change detection: If prevState === nextState, Redux and connected components assume nothing changed and skip work.
It enables cheap, correct re-renders:
A shallow === comparison is O(1) versus an expensive deep equality check on every update.
React-Redux uses this to decide whether a subscribed component should re-render.
It powers debugging features: Each state is a distinct snapshot, enabling time-travel and reliable action replay.
The consequence: Mutating in place keeps the same reference, so updates go undetected; you must create new objects/arrays (or let Immer do it).
Q29.What is createReducer, and how does the builder callback notation work?
createReducer, and how does the builder callback notation work?createReducer is an RTK utility that maps action types to case reducers without a manual switch statement, and it wraps everything in Immer. The recommended form is the builder callback, which gives you a typed builder object to register cases.
builder.addCase(actionCreator, reducer): Handles one specific action type; you can pass an RTK action creator directly for full type inference.
builder.addMatcher(predicate, reducer): Runs for any action matching a predicate (e.g. all .pending actions), useful for shared behavior.
builder.addDefaultCase(reducer): Fallback when nothing else matched, like the default of a switch.
Order matters: Cases are checked first, then matchers in order, then the default case.
Q30.How does configureStore differ from the traditional createStore, and what middleware does it include by default and why?
configureStore differ from the traditional createStore, and what middleware does it include by default and why?configureStore is Redux Toolkit's opinionated wrapper around createStore: it sets up the store with good defaults (sensible middleware, DevTools, easy reducer combining) in one call, whereas createStore is bare and requires you to wire everything manually.
Less boilerplate:
Accepts a reducer map object and calls combineReducers for you.
Enables the Redux DevTools extension automatically in development.
Default middleware from getDefaultMiddleware():
redux-thunk: lets you dispatch functions for async logic.
Immutability check: warns if you mutate state outside a reducer.
Serializability check: warns if non-serializable values (Promises, class instances) end up in state or actions.
Why those defaults:
They catch the most common Redux bugs early and cover the most common need (async), so a correct store works out of the box.
The dev-only checks are stripped in production for performance.
Q31.What does the store.subscribe method do, and how is it used under the hood by React-Redux?
store.subscribe method do, and how is it used under the hood by React-Redux?store.subscribe(listener) registers a callback that Redux calls after every dispatched action (after the state has updated), and it returns an unsubscribe function. It's the low-level notification mechanism that binds Redux state changes to the outside world.
What it does:
Runs the listener on every state change; the listener itself must call store.getState() to read new state (it receives no arguments).
Returns a function you call to remove the listener and avoid leaks.
How React-Redux uses it:
The <Provider> subscribes to the store and, on change, notifies subscribed components.
useSelector runs your selector after each change and forces a re-render only when the selected slice's reference changes.
It uses useSyncExternalStore under the hood to subscribe safely and avoid tearing in concurrent React.
Practical note: you rarely call subscribe directly in app code; React-Redux abstracts it.
Q32.What is preloaded (initial) state when configuring the store, and how is it different from a reducer's default state?
Preloaded state is an initial state value you hand to the store at creation time (the preloadedState option), used to hydrate the whole store. A reducer's default state is the fallback each reducer defines for its own slice when no state exists yet.
Preloaded state:
Set once when the store is created and applies to the entire state tree.
Common uses: server-side rendering hydration, restoring persisted state from localStorage, or seeding tests.
Reducer default state:
Declared per slice (e.g. initialState in createSlice, or a default parameter in a plain reducer).
Used only when that slice has no incoming value.
How they interact: Preloaded state takes precedence: where it provides a value, that value wins; where it's missing a slice, the reducer's default fills in.
Q33.What are the conceptual differences between the legacy connect HOC and the modern Hooks API? Are there any performance implications?
connect HOC and the modern Hooks API? Are there any performance implications?Both connect a component to the store, but connect is a higher-order component that injects state and dispatch as props, while the Hooks API (useSelector, useDispatch) reads them directly inside the component body. Hooks are the modern, simpler default.
Conceptual difference:
connect wraps a component, uses mapStateToProps/mapDispatchToProps, and keeps the component itself unaware of Redux (good for reuse/separation).
Hooks embed store access inline: less boilerplate, no wrapper component, easier with TypeScript.
Performance implications:
connect does a shallow comparison of all merged props by default, which can prevent renders across many values at once.
useSelector compares each selector's result with ===; multiple selectors give fine-grained control but you must avoid returning new references.
In practice both are comparably fast; Hooks avoid extra wrapper components in the tree.
Guidance: Use Hooks for new code; connect remains fully supported for legacy or class components.
Q34.What is the difference between mapDispatchToProps as an object versus a function?
mapDispatchToProps as an object versus a function?Both supply dispatch-bound action creators as props, but the object form is a shorthand that auto-wraps each creator in dispatch, while the function form gives you the dispatch reference directly for custom logic.
Object shorthand:
Each key's action creator is automatically wrapped with bindActionCreators, so calling the prop dispatches the action.
Concise and covers the common case; no access to dispatch or ownProps.
Function form:
Signature (dispatch, ownProps) => ({ ... }) gives full control.
Use when you need conditional logic, to dispatch multiple actions, or to combine ownProps into the payload.
Rule of thumb: Prefer the object form for simple mappings; drop to the function form only when you need custom dispatch behavior.
Q35.What is the purpose of the equalityFn in useSelector?
equalityFn in useSelector?The equalityFn is the optional second argument to useSelector that controls how the previous and current selector results are compared to decide whether to re-render. By default it's strict reference equality (===).
Why override the default: When your selector returns a new object or array each run, === always fails and you re-render needlessly.
Common choice: Pass shallowEqual from react-redux to compare object/array contents one level deep.
Signature: (previous, next) => boolean: return true to skip the render (treat as equal).
Caveats: Deep equality checks can be more expensive than an occasional re-render; prefer memoized selectors when possible.
Q36.Explain the difference between useDispatch and useStore. When would you ever need useStore?
useDispatch and useStore. When would you ever need useStore?useDispatch returns the store's dispatch function for sending actions, while useStore returns the entire store instance. You rarely need useStore; reach for it only in special cases where subscribing via useSelector isn't appropriate.
useDispatch: Everyday tool for dispatching actions; stable reference for the store's lifetime.
useStore:
Gives getState(), dispatch, subscribe, and replaceReducer.
Does NOT subscribe to updates, so reading store.getState() won't re-render on change.
When you actually need useStore:
Reading the latest state imperatively inside a callback without subscribing.
Replacing reducers dynamically (code-splitting) via replaceReducer.
Rule: if you want the component to react to state changes, use useSelector, not useStore.
Q37.What is mergeProps in connect, and when would you need to override the default merge behavior?
mergeProps in connect, and when would you need to override the default merge behavior?mergeProps is the optional third argument to connect that decides how the results of mapStateToProps, mapDispatchToProps, and the component's own props are combined into the final props. By default they're shallow-merged.
Default behavior: Equivalent to { ...ownProps, ...stateProps, ...dispatchProps }, with later sources winning on key collisions.
Signature: (stateProps, dispatchProps, ownProps) => finalProps.
When to override:
You need to combine state and dispatch into a single prop (e.g. build a handler that reads a state value and dispatches).
You want to depend on ownProps to pick which slice or action to expose.
You want to rename, filter, or restructure props rather than a flat merge.
Q38.What is shallowEqual in React-Redux, and when would you pass it to useSelector?
shallowEqual in React-Redux, and when would you pass it to useSelector?shallowEqual is an equality function React-Redux provides that compares two values one level deep, comparing each of their top-level keys with ===. You pass it as the second argument to useSelector so the component only re-renders when the selected fields actually change, not whenever a new object reference is returned.
The problem it solves:
useSelector re-renders when the selector result changes by reference (default ===).
A selector that returns a new object/array each call (e.g. { a, b }) always fails ===, causing needless re-renders.
How shallowEqual helps: It checks that both results have the same keys and each value is reference-equal, so a freshly built object with identical fields is treated as unchanged.
When to use it:
When the selector returns a new object or array assembled from multiple state slices.
Not needed if you select a single primitive value or a stable reference: default equality already works.
An alternative is a memoized selector (reselect) that returns a stable reference.
Q39.What is the batch function in React-Redux, and what problem did it solve before React 18's automatic batching?
batch function in React-Redux, and what problem did it solve before React 18's automatic batching?batch is a React-Redux utility that wraps multiple dispatches so React processes all the resulting state updates in a single render pass instead of one per dispatch. It mattered before React 18 because React only batched updates automatically inside React event handlers, not inside async callbacks, promises, or timers.
The pre-18 problem:
Multiple dispatches inside a setTimeout, promise, or async thunk each triggered a separate re-render.
That meant redundant renders and possibly rendering intermediate states.
What batch does: It uses React's unstable_batchedUpdates internally to group updates from the enclosed callback into one render.
React 18 changed things:
With automatic batching, updates in promises, timeouts, and native handlers are batched by default, so batch is largely redundant on React 18.
It's kept for backward compatibility and remains a no-op-style safe wrapper.
Q40.What is the purpose of middleware in Redux? Explain the signature store => next => action.
store => next => action.Middleware is a way to insert custom logic into Redux's dispatch pipeline: it sits between dispatching an action and the moment the reducer receives it. It's the standard extension point for side effects, logging, async logic, and crash reporting. The store => next => action shape is a curried chain of three functions, each capturing a different piece of context.
store (outer function):
Receives a subset of the store API ({ getState, dispatch }), so middleware can read state or re-dispatch.
Called once at setup time when the chain is built.
next (middle function):
A reference to the next middleware's action handler in the chain (or the real dispatch for the last one).
Calling next(action) passes the action along; not calling it stops the action.
action (inner function): The actual dispatched action; here you inspect, transform, delay, or swallow it before calling next.
Key idea: Each middleware wraps the next, forming an onion: logic runs before and after next(action).
Q41.What are the default middlewares included in RTK's configureStore, and why are they there?
configureStore, and why are they there?RTK's configureStore ships with a sensible default middleware stack via getDefaultMiddleware(): redux-thunk for async logic, plus (in development) an immutability check and a serializability check. They exist to enable common patterns and to catch bugs early.
redux-thunk:
Lets you dispatch functions for async/conditional logic out of the box.
Included in both dev and production.
Immutability check middleware:
Detects accidental state mutations between dispatches and warns you.
Dev only (removed in production for performance).
Serializability check middleware:
Warns when non-serializable values (Promises, class instances, functions) end up in the state or actions, protecting time-travel debugging and persistence.
Dev only.
Customizing them: You can tune or add to them via the callback form: middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(myMiddleware).
Q42.What exactly is a thunk, and how does the redux-thunk middleware work internally to allow dispatching functions?
redux-thunk middleware work internally to allow dispatching functions?A thunk is a function that wraps a deferred computation: in Redux it's a function you dispatch instead of a plain action object, giving you a place to run async or conditional logic. The redux-thunk middleware makes this possible by intercepting dispatched values and, if the value is a function, calling it rather than forwarding it to the reducer.
What gets dispatched: Normally an action object; with thunk you can dispatch a function (dispatch, getState) => { ... }.
How the middleware works:
It checks typeof action === 'function'.
If so, it calls the function with dispatch, getState (and an optional extra argument) and returns its result.
Otherwise it passes the action to next(action) as usual.
Why it's useful: The thunk can await async work and dispatch real actions when it resolves, and read current state to decide what to do.
Q43.How would you write a custom middleware such as a logger? Walk me through what happens at each layer of store => next => action.
store => next => action.A logger middleware follows the store => next => action pattern and logs around next(action): before the call you have the old state and the action, after it you have the new state. Each curried layer captures the context you need at that stage.
store layer: Runs once at setup; captures getState so we can read state later.
next layer: Runs once; captures the next handler so we can forward the action.
action layer:
Runs on every dispatch: log the action and previous state, call next(action) to let it reach the reducer, then log the resulting state.
Return the value of next(action) so downstream dispatch callers get the expected result.
Q44.Compare Redux Thunk and Redux Saga. When would you choose one over the other?
Redux Thunk and Redux Saga. When would you choose one over the other?Both are middleware for async logic, but Thunk is a minimal function-based approach for simple cases while Saga uses generators and effects to model complex, long-running async flows declaratively.
Redux Thunk:
An action creator returns a function that receives dispatch and getState; you write plain imperative async code.
Tiny, easy to learn, ideal for straightforward data fetching.
Harder to test, cancel, or coordinate complex sequences.
Redux Saga:
Uses generator functions that yield plain effect objects (call, put, take) the middleware executes.
Excels at cancellation, debouncing, concurrency control, and reacting to specific action patterns.
Steeper learning curve and more boilerplate.
When to choose:
Thunk (or createAsyncThunk): most apps with simple request/response flows.
Saga: complex orchestration, cancellation, polling, or event-driven workflows.
Q45.In Redux Saga, what is the difference between takeEvery and takeLatest? When would you use call vs put?
Redux Saga, what is the difference between takeEvery and takeLatest? When would you use call vs put?takeEvery runs a worker for every matching action concurrently, while takeLatest cancels any in-flight worker and keeps only the newest. call invokes a function (usually async), and put dispatches an action back to the store.
takeEvery:
Spawns a new task on each action; all run in parallel with no cancellation.
Use when every request matters (e.g. logging, independent adds).
takeLatest:
Cancels the previous task when a new matching action arrives; only the last result is applied.
Use for search-as-you-type or refreshes where stale responses should be discarded.
call vs put:
call(fn, ...args): invoke a (usually async) function and wait for its result; declarative and easy to test.
put(action): dispatch an action to update the store, typically after call resolves.
Q46.What is createAsyncThunk, and how does it handle the lifecycle of an asynchronous request (pending, fulfilled, rejected)?
createAsyncThunk, and how does it handle the lifecycle of an asynchronous request (pending, fulfilled, rejected)?createAsyncThunk is an RTK helper that wraps an async function and automatically dispatches three lifecycle actions (pending, fulfilled, rejected) based on the returned Promise, so you only handle those cases in your reducer.
You provide two things:
A type prefix string (e.g. 'users/fetchById').
A payload creator: an async function returning the data (or throwing/rejecting on error).
Lifecycle actions dispatched automatically:
pending: dispatched immediately (set loading state).
fulfilled: the resolved value becomes action.payload.
rejected: the error info populates action.error (or rejectWithValue for custom payloads).
Handle them in extraReducers using .addCase for each state.
Q47.In Redux Saga, what is the difference between the call and put effects?
Redux Saga, what is the difference between the call and put effects?call tells the middleware to invoke a function (typically an async API call) and wait for its result, while put tells it to dispatch an action into the Redux store. Both return plain effect descriptor objects rather than executing directly.
call:
call(fn, ...args) describes 'run this function with these args'; the saga pauses until the Promise resolves.
Used for side effects: API requests, delays, other sagas.
put:
put(action) describes 'dispatch this action', the Saga equivalent of calling dispatch.
Used to feed results back into reducers.
Why descriptors matter: Because effects are just objects, tests can assert what a saga yields without actually hitting the network.
Q48.How do you handle errors in a createAsyncThunk, and what does rejectWithValue do?
createAsyncThunk, and what does rejectWithValue do?Inside a createAsyncThunk payload creator, any thrown error causes the thunk to dispatch its rejected action. By default the error goes into action.error, but rejectWithValue() lets you return a custom, serializable payload as action.payload instead.
Default error handling: A thrown/rejected error is caught, and RTK serializes it into action.error (a plain object with message, name, stack).
rejectWithValue(value):
Returns a value that becomes action.payload in the rejected case, ideal for server validation errors or response bodies.
Signals a controlled rejection rather than an unexpected crash.
Access API errors via thunkAPI: The second arg to the payload creator gives you rejectWithValue, getState, dispatch, and signal.
Q49.What does unwrap() do on the promise returned by dispatching a createAsyncThunk?
unwrap() do on the promise returned by dispatching a createAsyncThunk?Dispatching a thunk returns a promise that always resolves (to the fulfilled or rejected action). Calling .unwrap() on it returns a new promise that resolves with the actual payload on success or throws the error on failure, letting you use normal try/catch.
Without unwrap: You get the action object back and must inspect it manually (e.g. if (fetchUser.rejected.match(result))).
With unwrap:
Success: resolves to action.payload directly.
Failure: rejects/throws with the rejectWithValue payload or serialized error.
Why it matters: Lets components react to thunk outcomes (navigate, show toast) with idiomatic error handling.
Q50.What is createEntityAdapter, and when would you use it over a standard array or object in your state?
createEntityAdapter, and when would you use it over a standard array or object in your state?createEntityAdapter is a helper for storing collections in a normalized shape (an { ids: [], entities: {} } structure) and gives you prebuilt CRUD reducers and memoized selectors. Use it when you manage lists of items keyed by id and need fast lookups and updates.
Normalized state: ids preserves order/sorting; entities maps id to item for O(1) access.
Prebuilt CRUD reducers: addOne, upsertMany, updateOne, removeOne, etc., usable inside reducers or extraReducers.
Generated selectors: getSelectors() yields selectAll, selectById, selectIds, memoized for performance.
When to prefer it over a plain array: Avoids repeated .find() scans and awkward immutable array splicing when you frequently update/remove by id.
Q51.What are serializability checks in RTK, and why should you avoid putting non-serializable data like Promises or class instances in the store?
RTK includes a development middleware that scans dispatched actions and store state for non-serializable values (Promises, class instances, functions, Map/Set, Dates) and warns you. Redux expects state and actions to be plain serializable data so features like time-travel debugging, persistence, and predictable replay work reliably.
What the check does: serializableCheck middleware logs a console warning when it finds a non-serializable value in state or an action.
Why serializability matters:
DevTools time-travel needs to snapshot/rehydrate state as JSON.
Persistence (localStorage, SSR) requires serializing to a string.
Non-plain values (Promises, class instances) can't be reliably reconstructed and break these guarantees.
Practical handling:
Store IDs/plain data, not the Promise or instance; resolve async work in thunks and store the result.
If truly necessary, configure ignoredActions/ignoredPaths to whitelist exceptions.
Q52.What is the purpose of the extraReducers builder callback in a slice?
extraReducers builder callback in a slice?extraReducers lets a slice respond to actions defined outside itself, most commonly the pending/fulfilled/rejected actions from createAsyncThunk or actions from other slices. The builder callback provides a type-safe API for adding these case matchers.
reducers vs extraReducers: reducers defines and owns its actions; extraReducers only listens to external actions (no new action creators generated).
Builder methods:
builder.addCase(actionCreator, reducer) for a specific action.
builder.addMatcher(predicate, reducer) for groups (e.g. all rejected thunks).
builder.addDefaultCase(reducer) as a fallback.
Typical use: Handling async thunk lifecycle to set loading/error/data.
Q53.What is a memoized selector, and why is it important to use libraries like Reselect or createSelector for derived data?
Reselect or createSelector for derived data?A memoized selector is a function that computes derived data from the store but caches its result, recomputing only when its inputs change. Libraries like Reselect (via createSelector) provide this so expensive transformations don't rerun on every render and don't hand back new object references that trigger needless re-renders.
What memoization solves:
Deriving data (filtering, sorting, aggregating) on every dispatch or render is wasteful if inputs are unchanged.
A plain selector that returns .filter() or .map() creates a new array each call, so useSelector's reference equality check fails and the component re-renders needlessly.
How createSelector works:
It takes input selectors plus a result function; it recomputes only when an input's output changes (by reference equality).
If inputs are unchanged, it returns the cached result, preserving the same reference.
Why it matters in interviews: Stable references prevent render cascades; cached computation keeps derived data cheap.
Q54.What is the difference between an input selector and an output selector?
In Reselect, input selectors extract raw pieces of state, and the output selector (the result function) computes the derived value from those pieces. Memoization is driven by the input selectors: if their outputs are unchanged by reference, the output selector is skipped and the cached result is returned.
Input selectors:
Simple, usually non-memoized functions like state => state.todos.
Their return values are the arguments passed to the output function, and their reference equality decides whether recomputation happens.
Output selector:
The final argument to createSelector: it receives the input results and produces the derived value.
It only runs when at least one input changed, so keep expensive work here.
Key implication: Input selectors should be cheap and should not create new references (avoid .map() inside an input selector, or memoization breaks).
Q55.Why should derived or computed data be kept out of the store, and computed via selectors instead?
Derived data (totals, filtered lists, computed flags) should be calculated from the source state via selectors rather than stored, because storing it creates a second source of truth that must be manually kept in sync. Selectors compute it on demand and memoize it, so it is always consistent and updated automatically.
Single source of truth: If a total is stored separately, every mutation to the source must also update the total, or they drift out of sync.
Less reducer complexity and fewer bugs: No extra actions/reducer logic just to maintain redundant fields.
Smaller, normalized state: Store only raw entities; compute views on top of them.
Cheap when memoized: With createSelector, the computation reruns only when inputs change, so on-the-fly derivation isn't a performance concern.
Q56.Explain the concept of tags and invalidation in RTK Query. How do they automate cache refreshing?
RTK Query. How do they automate cache refreshing?In RTK Query, tags are labels that connect queries (which cache data) to mutations (which change data). A query declares the tags it provides, a mutation declares the tags it invalidates, and when a mutation runs, RTK Query automatically refetches any query holding an invalidated tag.
providesTags: A query says "I represent this data," e.g. ['Post'] or per-item { type: 'Post', id }.
invalidatesTags: A mutation says "I changed this data," marking matching cached queries as stale.
Automatic refetch: Any mounted query whose provided tags were invalidated is refetched, so the UI stays fresh without manual dispatches.
Granularity: Use id-specific tags (and a LIST id) to invalidate precisely instead of refetching everything.
Q57.Why would you use RTK Query instead of manually managing data fetching with createAsyncThunk and slices?
RTK Query instead of manually managing data fetching with createAsyncThunk and slices?RTK Query is a purpose-built data-fetching and caching layer that eliminates the large amount of boilerplate you write by hand with createAsyncThunk and slices (loading flags, cache logic, dedup, refetching). You describe endpoints declaratively and get hooks, caching, and cache invalidation for free.
Eliminates boilerplate: No manual pending/fulfilled/rejected reducers or isLoading/error state to maintain per request.
Automatic caching and dedup: Identical requests share one cache entry; duplicate in-flight requests are deduplicated.
Cache lifecycle management: Tag-based invalidation, refetch on focus/reconnect, and automatic cache cleanup when no component uses the data.
Generated hooks: Endpoints produce ready-to-use useGetXQuery / useXMutation hooks with full loading/error state.
When thunks still make sense: Non-server state or complex custom async workflows not centered on fetching/caching.
Q58.Why does RTK Query generate hooks automatically, and what state do these hooks provide besides the data?
RTK Query generate hooks automatically, and what state do these hooks provide besides the data?RTK Query generates hooks automatically because each endpoint you define is a complete, self-contained data unit: given the endpoint definition it can produce a React hook that handles fetching, caching, subscription, and re-rendering for you. The hook returns not just data but a full snapshot of the request's status.
Why hooks are generated:
You describe endpoints once; RTK Query derives useGetPostsQuery, useAddPostMutation, etc.
This removes hand-written thunks, action types, reducers, and loading flags.
State beyond data:
Status booleans: isLoading (first fetch), isFetching (any in-flight fetch, including refetches), isSuccess, isError.
error: the serialized error when the request fails.
refetch: a function to manually re-run the query.
currentData: data for the current arg only (undefined while a new arg loads).
Interview point: distinguish isLoading from isFetching: isLoading is true only when there is no cached data yet; isFetching is true on every fetch, useful for showing a subtle refresh spinner over stale data.
Q59.What is fetchBaseQuery in RTK Query, and how does the baseQuery concept work?
fetchBaseQuery in RTK Query, and how does the baseQuery concept work?A baseQuery is the single function every endpoint uses to actually talk to the server: it receives the endpoint's args and returns { data } or { error }. fetchBaseQuery is a small built-in wrapper around fetch that RTK Query provides for common HTTP needs.
The baseQuery concept:
It's the shared transport layer; endpoints define query args, and baseQuery turns them into a request/response.
You can swap it for axios, GraphQL, or a fully custom function, so RTK Query isn't tied to fetch.
What fetchBaseQuery gives you:
A baseUrl prefix and automatic JSON parse/serialize.
prepareHeaders to inject auth tokens or common headers.
Normalizes non-2xx responses into the error shape.
Extending it: Wrap fetchBaseQuery in a baseQueryWithReauth function to handle token refresh/retry on 401.
Q60.How does polling work in RTK Query, and how do you configure a query to refetch on an interval?
RTK Query, and how do you configure a query to refetch on an interval?Polling makes a query automatically refetch on a fixed interval while it has active subscribers. You enable it by passing pollingInterval (in milliseconds) to the query hook.
How to configure it:
Pass options as the hook's second argument: useGetMessagesQuery(arg, { pollingInterval: 5000 }).
Set it to 0 (default) to disable polling.
How it behaves:
The timer runs only while the component is mounted/subscribed; unmounting stops it.
Each poll sets isFetching true while keeping the previous data visible.
skipPollingIfUnfocused can pause polling when the tab isn't focused.
Dynamic control: Because it's just an option, you can compute the interval from state to speed up, slow down, or stop polling reactively.
Q61.How do you conditionally run or skip a query in RTK Query when its arguments aren't ready yet?
RTK Query when its arguments aren't ready yet?You use the skip option (or the skipToken sentinel) to tell a query hook not to fire until its arguments are ready. The hook still returns a status object, just with no request in flight.
Using skip:
Pass { skip: true } as an option: useGetUserQuery(userId, { skip: !userId }).
While skipped, no request runs and status flags stay in an idle/uninitialized state.
Using skipToken:
Pass skipToken as the arg itself: useGetUserQuery(userId ?? skipToken).
This is TypeScript-friendly: it narrows the arg type so you never pass undefined to an endpoint that requires a value.
Why it matters:
Prevents wasted or invalid requests (e.g. fetching before an ID or auth token exists).
When the condition flips to ready, the query fires automatically.
Q62.What is a lazy query in RTK Query, and when would you use it instead of the standard auto-firing hook?
RTK Query, and when would you use it instead of the standard auto-firing hook?A lazy query is one you trigger manually instead of automatically on render. You get it from the generated useLazyGetXQuery hook, which returns a trigger function and the result object, so the fetch runs only when you call it.
Standard vs lazy:
useGetXQuery(arg) fires on mount and refires when the arg changes.
useLazyGetXQuery() returns [trigger, result]; nothing runs until you invoke trigger(arg).
When to use lazy:
Event-driven fetches: on button click, form submit, or search-as-you-type.
When the arg isn't known until user action, and skip would be awkward.
When you need the result imperatively: the trigger returns a promise you can await and unwrap().
Note: Lazy triggers still populate and read from the same cache, so subsequent calls can serve cached data.
Q63.What does it mean to normalize Redux state? Why is it better to store relational data by ID rather than nesting objects?
Normalizing state means storing collections as a flat lookup table keyed by ID plus an array of IDs, rather than deeply nested objects, so each entity lives in exactly one place.
The normalized shape: Typically { byId: { [id]: entity }, allIds: [...] }; relations reference other entities by ID, not by embedding them.
Single source of truth:
An entity appearing in many lists is stored once, so an update touches one record instead of hunting through nested copies.
Nesting causes duplication and stale/inconsistent data when copies drift apart.
Cheaper updates and lookups: O(1) access by ID, and immutable updates avoid rewriting deep trees (fewer wasted re-renders).
Tooling: createEntityAdapter from Redux Toolkit generates this shape plus CRUD reducers and memoized selectors for you.
Q64.What is the 'Ducks' pattern in Redux file structure?
The "Ducks" pattern bundles everything for one feature (action types, action creators, and reducer) into a single module instead of splitting them across actions/, reducers/, and types/ folders.
Core rules:
The module must export default its reducer.
It must export its action creators as named exports.
Action types are namespaced (e.g. app/feature/ACTION) to avoid collisions.
Why it helps: Feature logic is co-located, so adding or removing a feature touches one file, improving cohesion and reducing import juggling.
Modern relevance: Redux Toolkit's createSlice is essentially a formalized Ducks: one file generates the reducer and action creators together.
Q65.How do you handle persistence in Redux? Explain the concept of rehydration.
Persistence means saving store state to durable storage and restoring it on startup; rehydration is the step where that saved state is read back and merged into the store as its initial state.
Saving: Subscribe to the store (or use redux-persist) and serialize chosen slices to localStorage, IndexedDB, etc. Persist only what you need.
Rehydration:
On load, read and parse the stored state and feed it in via preloadedState, or let redux-persist dispatch a REHYDRATE action that reducers merge.
Because it can be async, you may briefly show a loader (PersistGate) until state is restored.
Correctness concerns:
Version and migrate persisted state so an old shape doesn't crash new reducers.
Validate/whitelist to avoid restoring stale or sensitive data.
Q66.When would you choose Redux over the React Context API, and in what scenarios is Redux considered overkill?
Context API, and in what scenarios is Redux considered overkill?Choose Redux when you have frequently-changing, shared, complex global state that benefits from predictable updates, middleware, and debugging; Context is a dependency-injection tool for low-frequency values, and reaching for Redux on a small app is usually overkill.
Prefer Redux when:
State is updated often and read by many distant components (cart, real-time data).
You need time-travel debugging, action logging, or the Redux DevTools.
Update logic is complex and benefits from middleware (async flows, side effects).
You want a single, testable, predictable state container with clear data flow.
Prefer Context when:
You pass rarely-changing values down the tree: theme, locale, current user, auth token.
The goal is avoiding prop-drilling, not managing high-frequency updates.
Redux is overkill when:
The app is small or state is mostly local; the boilerplate outweighs the benefit.
Note: Context re-renders all consumers on value change and has no built-in memoization or middleware, so it is not a Redux replacement for hot state.
Q67.How do you decide whether a piece of state should reside in local component state, a parent component, or the global Redux store?
Push state as low as possible: keep it local by default, lift it to a shared parent only when siblings need it, and promote it to Redux only when it is truly global, cross-cutting, or must survive across routes.
Local component state (useState): Only that component cares: input values, toggles, hover/open state, form drafts.
Lifted to a parent (props): A few closely-related siblings must share it and the common parent is nearby: lifting state up is simpler than global.
Global Redux store:
Many unrelated components across the tree read/write it, or it must persist across route changes (auth, cart, cached server data).
Updates are complex or need debugging/middleware visibility.
Guiding questions: "Who reads it?" and "How far do they sit apart?" drive the decision; do not globalize state just because it is convenient.
Q68.Why are Redux reducers considered easy to test, and what is the recommended approach for testing a component that is heavily integrated with the Redux store?
Reducers are trivial to test because they are pure functions: given the same state and action they always return the same new state, with no side effects, DOM, or mocks required. For components tied to Redux, test them integrated by rendering with a real store rather than mocking selectors.
Why reducers are easy:
Pure and deterministic: (state, action) => newState with no I/O or hidden dependencies.
Test is just: call the reducer with an input state and action, assert the output.
Testing connected components:
Wrap the component in a real <Provider> with a store preloaded to the needed state (integration-style, closer to real behavior).
Prefer testing behavior via the UI (React Testing Library): dispatch through user interactions and assert rendered output.
A common RTK pattern is a renderWithProviders helper that builds a fresh store per test with preloadedState.
Q69.How do you type the Redux store in TypeScript, deriving RootState and AppDispatch from the store?
TypeScript, deriving RootState and AppDispatch from the store?Configure the store first, then infer the types from it: derive RootState from the store's getState return type and AppDispatch from the store's dispatch type. This keeps types automatically in sync as slices are added.
Infer, do not hand-write:
Use ReturnType<typeof store.getState> so new reducers extend RootState automatically.
Use typeof store.dispatch for AppDispatch so thunk/async dispatch signatures are captured.
Why derive from the store: Manually typed state drifts from reality; inference guarantees the types match the actual configured reducers and middleware.
Q70.Why is it recommended to create pre-typed useAppSelector and useAppDispatch hooks instead of the plain ones?
useAppSelector and useAppDispatch hooks instead of the plain ones?Because the plain useSelector and useDispatch hooks don't know your app's types, you'd have to annotate RootState and AppDispatch at every single call site. Pre-typed versions bake those types in once, giving you autocomplete and type safety everywhere for free.
Avoids repetitive annotation: Without it every selector needs useSelector((state: RootState) => ...); the typed hook infers it automatically.
Captures the full dispatch type: Plain useDispatch doesn't know about thunks; AppDispatch makes dispatching async thunks type-safe.
Single source of truth: Define once in a hooks file and import app-wide, so type changes propagate automatically.
Q71.Explain Time Travel Debugging. What architectural features of Redux make this possible?
Time travel debugging is the ability to step backward and forward through your app's state history, replaying or undoing actions to see exactly how the UI got to a given state. Redux enables it because state transitions are pure and fully described by a serializable action log.
What it does: Tools like the Redux DevTools record each action and the resulting state, letting you jump to any past state or replay the sequence.
Pure reducers make replay deterministic: Since (state, action) => newState has no side effects, re-running the same actions reproduces the same states exactly.
Immutable state snapshots: Each change creates a new state object, so past states are preserved intact and can be re-applied to the store.
Serializable plain-object actions: Actions are just data, so the full history can be logged, exported, and replayed.
Single store: One state tree means one timeline to record and restore, not many independent stores to coordinate.
Q72.What are the advantages and disadvantages of having a single global store versus multiple stores as seen in Flux?
Q73.What is the 'shared reference' bug, and how does Redux's immutable pattern prevent it?
Q74.What are the current and original helpers from Immer, and when would you use them inside a reducer?
current and original helpers from Immer, and when would you use them inside a reducer?Q75.What does store.replaceReducer do, and how does it enable code-splitting or dynamically injecting reducers?
store.replaceReducer do, and how does it enable code-splitting or dynamically injecting reducers?Q76.How does useSelector determine when a component should re-render, and what happens if your selector returns a new object reference on every call?
useSelector determine when a component should re-render, and what happens if your selector returns a new object reference on every call?Q77.How does React-Redux handle multiple dispatches in a single event? Explain how it batches updates to prevent unnecessary re-renders.
Q78.What is the difference between a Redux Middleware and a Store Enhancer?
Q79.Does the order in which you register middleware in applyMiddleware matter? Why or why not?
applyMiddleware matter? Why or why not?Q80.How do you handle race conditions in Redux, such as two API calls returning in the wrong order?
Q81.What is the Listener Middleware in RTK, and why is it often recommended as a lighter alternative to Sagas?
Listener Middleware in RTK, and why is it often recommended as a lighter alternative to Sagas?Q82.At a conceptual level, what is redux-observable, and how do epics differ from thunks and sagas?
redux-observable, and how do epics differ from thunks and sagas?Q83.What is combineSlices, and how does it support lazy loading of reducers compared to the old combineReducers?
combineSlices, and how does it support lazy loading of reducers compared to the old combineReducers?Q84.How do you handle selectors that need arguments while maintaining memoization?
Q85.Reselect's default createSelector only caches the last result. What problem does this cause when the same selector is shared across multiple component instances, and how do you fix it?
createSelector only caches the last result. What problem does this cause when the same selector is shared across multiple component instances, and how do you fix it?Q86.What are optimistic updates, and how would you conceptually implement them using RTK Query?
RTK Query?Q87.Why would you use RTK Query instead of a standalone library like TanStack Query if your app already uses Redux?
RTK Query instead of a standalone library like TanStack Query if your app already uses Redux?Q88.How does RTK Query manage the cache lifetime of data? What happens when the last component using a query unmounts?
RTK Query manage the cache lifetime of data? What happens when the last component using a query unmounts?Q89.What do transformResponse and selectFromResult do in RTK Query, and why are they useful?
transformResponse and selectFromResult do in RTK Query, and why are they useful?Q90.How does injectEndpoints support code-splitting an RTK Query API definition across features?
injectEndpoints support code-splitting an RTK Query API definition across features?Q91.How do you handle the Redux store during SSR? Explain the process of hydration and why the server's state must be serialized to the client.
SSR? Explain the process of hydration and why the server's state must be serialized to the client.Q92.What are the trade-offs of using redux-persist?
redux-persist?Q93.How would you handle a large state tree to ensure that a small update in one corner doesn't slow down the entire app?
Q94.What are the best practices for designing a scalable Redux state shape for a large enterprise application?
Q95.With the rise of lighter libraries like Zustand or the Signals pattern, why would a team still choose Redux for a new project?
Zustand or the Signals pattern, why would a team still choose Redux for a new project?