Redux Interview Questions
Crack your Redux interview with 28 in-depth questions covering store, actions, reducers, middleware, Redux Toolkit, RTK Query, Reselect, and real-world patterns - explained in plain English with code examples.
I. Beginner Level
1. What is Redux and why would you use it?
Redux is a predictable state management library for JavaScript applications. Most people use it with React, but it works with any UI framework or even vanilla JS. The core idea is simple: instead of spreading your application's data across dozens of components, you put it all in one central place - the store - and every component that needs data reads from there.
Here's the problem it's actually solving. As your React app grows, you start running into situations where a piece of state is needed by components that are far apart in the component tree. You end up passing props down through 5 or 6 intermediate components that don't actually use the data - they're just passing it along. This is called props drilling, and it gets painful fast. Redux sidesteps this entirely.
The other thing Redux gives you is predictability. Because state can only change in one specific, controlled way, you always know exactly what caused a state change and when. This makes debugging significantly easier - especially when things go wrong in production.
Centralised state: One store holds all your application data. No more hunting through components to find where a piece of state lives.
Predictable updates: State only changes via actions and reducers - a strict, traceable pattern.
Powerful developer tools: Time-travel debugging, action log, state snapshots - all out of the box.
Testability: Pure reducers are trivially easy to unit test - input in, output out, no side effects.
2. What are the three core principles of Redux?
Redux is built on three foundational rules. Every architectural decision in Redux - why actions exist, why reducers must be pure, why there's only one store - comes from these three principles. Know these and you understand Redux at a deeper level than most people who just copy-paste it.
| Principle | What it means | Why it matters |
|---|---|---|
| Single Source of Truth | The entire application state lives in one store object | Every component reads from the same source - no conflicting versions of the same data |
| State is Read-Only | You cannot directly mutate state - you dispatch an action to describe the change | Prevents accidental mutations and makes state changes traceable - every change has a paper trail |
| Changes via Pure Functions | Reducers must be pure functions - same input always produces the same output, no side effects | Makes state transitions predictable, testable, and replayable (time-travel debugging) |
3. What is a store in Redux?
The store is the single JavaScript object that holds your entire application state. Think of it as the single source of truth for your app - a central warehouse where all your data lives. There is only ever one store in a Redux application.
The store does three things: it holds the current state tree, it lets you dispatch actions to change that state, and it lets components subscribe to state changes so they can re-render when relevant data updates. In modern Redux Toolkit, you create the store using configureStore.
1import { configureStore } from '@reduxjs/toolkit';
2import cartReducer from './features/cart/cartSlice';
3import authReducer from './features/auth/authSlice';
4import productsReducer from './features/products/productsSlice';
5
6// The store - created once, at the top of your app
7export const store = configureStore({
8 reducer: {
9 cart: cartReducer, // manages cart state
10 auth: authReducer, // manages user auth state
11 products: productsReducer, // manages product list state
12 },
13});
14
15// TypeScript types - generated automatically from the store
16export type RootState = ReturnType<typeof store.getState>;
17export type AppDispatch = typeof store.dispatch;
18
19// What the state tree looks like:
20// {
21// cart: { items: [], total: 0 },
22// auth: { user: null, isLoggedIn: false },
23// products: { list: [], loading: false, error: null }
24// }
254. What is an action in Redux?
An action is a plain JavaScript object that describes something that happened in your application. It is the only way to send data to the Redux store. You can think of an action as a message you send to the store saying 'hey, this event just occurred - here's what changed'.
Every action must have a type property - a string that identifies what kind of event it is. By convention, types are written like 'feature/eventName'. An action can optionally carry extra data in a payload property - whatever the reducer needs to update the state correctly.
1// Actions are plain objects - they just describe WHAT happened
2// They don't do anything themselves
3
4// Simple action - no extra data needed
5{ type: 'cart/clearCart' }
6
7// Action with payload - carries the data the reducer needs
8{ type: 'cart/addItem', payload: { id: 42, name: 'Laptop', price: 75000 } }
9
10{ type: 'auth/loginSuccess', payload: { user: { id: 1, name: 'Alice' }, token: 'abc123' } }
11
12{ type: 'products/setFilter', payload: { category: 'electronics', priceRange: [0, 50000] } }
13
14// With Redux Toolkit, action creators are auto-generated for you
15// You rarely write these objects by hand
16import { addItem } from './cartSlice';
17addItem({ id: 42, name: 'Laptop', price: 75000 });
18// Produces: { type: 'cart/addItem', payload: { id: 42, name: 'Laptop', price: 75000 } }
195. What is a reducer in Redux?
A reducer is a pure function that takes the current state and an action, and returns the next state. It is the only place where state actually changes in Redux. The name comes from the JavaScript Array.reduce() method - same concept of taking inputs and producing a single output.
The critical rules for reducers: they must be pure functions (no API calls, no random numbers, no Date.now()), they must never mutate the existing state directly (always return a new object), and they must return the current state unchanged for any action they don't recognise.
1// Vanilla Redux reducer (without Toolkit)
2const initialState = {
3 items: [],
4 total: 0,
5};
6
7function cartReducer(state = initialState, action) {
8 switch (action.type) {
9 case 'cart/addItem':
10 return {
11 ...state, // never mutate - spread the old state
12 items: [...state.items, action.payload],
13 total: state.total + action.payload.price,
14 };
15
16 case 'cart/removeItem':
17 const removed = state.items.find(i => i.id === action.payload.id);
18 return {
19 ...state,
20 items: state.items.filter(i => i.id !== action.payload.id),
21 total: state.total - (removed?.price || 0),
22 };
23
24 case 'cart/clearCart':
25 return initialState; // reset to initial state
26
27 default:
28 return state; // ALWAYS return state for unknown actions
29 }
30}
31
32// Rules a reducer must never break:
33// ✗ No API calls inside a reducer
34// ✗ No mutating state: state.items.push(item) - WRONG
35// ✗ No side effects: console.log, Math.random(), Date.now()
36// ✅ Always return new objects/arrays, never modify existing ones
376. What is dispatch in Redux?
dispatch is the function that sends an action to the Redux store. It's the only way to trigger a state change. When you dispatch an action, Redux passes it to the root reducer, which processes it and returns the new state. The store then updates, and any components subscribed to the relevant slice of state re-render.
Think of dispatch like making an order at a restaurant. You (the component) tell the waiter (dispatch) what you want. The waiter takes the order to the kitchen (reducer). The kitchen prepares the food (computes new state). The waiter brings it back to you (UI updates). You never go into the kitchen yourself - you always go through dispatch.
1import { useDispatch } from 'react-redux';
2import { addItem, removeItem, clearCart } from './cartSlice';
3
4function ProductCard({ product }) {
5 const dispatch = useDispatch();
6
7 const handleAddToCart = () => {
8 // Dispatch sends the action to the store
9 dispatch(addItem({ id: product.id, name: product.name, price: product.price }));
10 };
11
12 const handleRemove = () => {
13 dispatch(removeItem({ id: product.id }));
14 };
15
16 const handleClear = () => {
17 dispatch(clearCart()); // action with no payload
18 };
19
20 return (
21 <div>
22 <h3>{product.name}</h3>
23 <button onClick={handleAddToCart}>Add to Cart</button>
24 <button onClick={handleRemove}>Remove</button>
25 </div>
26 );
27}
287. What is a selector in Redux?
A selector is a function that extracts a specific piece of data from the Redux store. Instead of reaching into the raw state tree directly in every component, you write a named selector function that knows how to pull out the data you need. This keeps your component code cleaner, and if your state structure ever changes, you fix it in one selector rather than hunting through every component.
1// selectors.js - define once, use anywhere
2export const selectCartItems = state => state.cart.items;
3export const selectCartTotal = state => state.cart.total;
4export const selectCartCount = state => state.cart.items.length;
5export const selectIsLoggedIn = state => state.auth.isLoggedIn;
6export const selectCurrentUser = state => state.auth.user;
7
8// Derived selector - computes from state
9export const selectCartItemsWithDiscount = state =>
10 state.cart.items.map(item => ({
11 ...item,
12 discountedPrice: item.price * 0.9,
13 }));
14
15// Using selectors in components
16function CartSummary() {
17 const items = useSelector(selectCartItems);
18 const total = useSelector(selectCartTotal);
19 const count = useSelector(selectCartCount);
20
21 // ✗ Avoid reaching into raw state directly in components
22 // const items = useSelector(state => state.cart.items);
23 // If you rename 'cart' to 'shoppingCart', you'd have to update every component
24
25 return (
26 <div>
27 <p>{count} items - ₹{total}</p>
28 </div>
29 );
30}
318. How does data flow in a Redux application?
Redux has a strict one-way (unidirectional) data flow. This is what makes it predictable - state always moves in the same direction. Understanding this flow is fundamental to debugging Redux apps.
1User Interaction (click, type, submit)
2 │
3 ▼
4 Component calls dispatch(action)
5 │
6 ▼
7 Middleware (Thunk / Saga) - optional
8 Handles async logic before passing action to reducer
9 │
10 ▼
11 Root Reducer receives (currentState, action)
12 Calls the correct slice reducer based on action.type
13 │
14 ▼
15 Slice Reducer returns NEW state
16 (never mutates old state - always a new object)
17 │
18 ▼
19 Store updates with the new state
20 │
21 ▼
22 useSelector hooks detect state change
23 Components that depend on this state re-render
24 │
25 ▼
26 UI reflects the new state
27The flow is always: UI → Action → Middleware → Reducer → Store → UI. Never in the other direction. This predictability is why Redux apps are easier to reason about and debug than component-local state spread across dozens of files.
9. When should you use Redux and when should you not?
This is a question where honest, nuanced answers score much better than 'always use Redux for everything'. Not every app needs Redux. It adds boilerplate, setup time, and a learning curve. Knowing when it's worth it and when it isn't shows maturity as a developer.
| Use Redux when... | Skip Redux when... |
|---|---|
| Many components across the app need access to the same state | The app is small - 3 to 5 components with simple local state |
| State updates in complex, interconnected ways | State only needs to flow between a parent and a few direct children |
| You need powerful debugging (time-travel, action replay) | The team isn't familiar with Redux and the project deadline is tight |
| Large team needs a consistent, standardised state pattern | Context API or Zustand would solve the problem with far less setup |
| You need state to persist (with redux-persist) or be serialisable | Most state is server data - RTK Query or React Query would be better |
10. What is the difference between Redux and React Context API?
Both Redux and Context API solve the same surface problem - prop drilling. But they're designed for different scales and different types of problems. Context is a data distribution mechanism. Redux is a full state management system.
| Feature | React Context API | Redux |
|---|---|---|
| Purpose | Share values down the component tree without prop drilling | Centralised, predictable state management with a strict update pattern |
| Performance | All consumers re-render when any value in the context changes | Components only re-render when the specific state they select changes |
| DevTools | None - no built-in debugging support | Redux DevTools - time-travel, action log, state snapshots |
| Async handling | Manual - manage loading/error states yourself | Built-in with createAsyncThunk and RTK Query |
| Boilerplate | Minimal setup | More setup - but Redux Toolkit has dramatically reduced this |
| Best for | Theming, auth state, locale - infrequently changing global data | Complex app state - cart, notifications, filters, UI state across features |
II. Intermediate Level
1. What is Redux Toolkit and why was it created?
Redux Toolkit (RTK) is the official, opinionated toolset for Redux development. It was created because vanilla Redux required an enormous amount of boilerplate - separate action type constants, action creator functions, verbose switch-case reducers, manual immutable update logic. A simple feature could require 4 or 5 files. That put a lot of people off Redux entirely.
Redux Toolkit solves this by bundling the best practices and most-used packages into one clean API. It includes configureStore (store setup), createSlice (reducers + action creators in one), createAsyncThunk (async operations), and RTK Query (data fetching). The Redux team now officially recommends it for all new Redux projects - vanilla Redux is considered legacy.
| Vanilla Redux pain point | RTK solution |
|---|---|
| Lots of boilerplate for actions + reducers | createSlice generates both in one function |
| Manually spreading state for every update (immutability) | Immer is built in - write mutating syntax, RTK makes it immutable |
| Complex async logic with thunks and loading/error tracking | createAsyncThunk handles the async flow and auto-generates lifecycle actions |
| Manual store configuration with multiple middleware installs | configureStore adds Thunk, DevTools, and serialisation checks automatically |
2. What is createSlice in Redux Toolkit?
createSlice is the star feature of Redux Toolkit. It takes a single configuration object and generates the reducer function, action type strings, and action creator functions all at once. What previously took 3 separate files now lives in one clean slice file.
The name 'slice' comes from the idea that you're defining a 'slice' of the overall application state - the cart slice, the auth slice, the products slice. Each slice manages its own isolated portion of the state tree.
1import { createSlice } from '@reduxjs/toolkit';
2
3const cartSlice = createSlice({
4 name: 'cart', // prefix for action type strings
5 initialState: {
6 items: [],
7 total: 0,
8 isOpen: false,
9 },
10 reducers: {
11 // Looks like mutation - but Immer makes it safe (more on this later)
12 addItem(state, action) {
13 const existing = state.items.find(i => i.id === action.payload.id);
14 if (existing) {
15 existing.quantity += 1;
16 } else {
17 state.items.push({ ...action.payload, quantity: 1 });
18 }
19 state.total += action.payload.price;
20 },
21
22 removeItem(state, action) {
23 const item = state.items.find(i => i.id === action.payload);
24 if (item) state.total -= item.price * item.quantity;
25 state.items = state.items.filter(i => i.id !== action.payload);
26 },
27
28 clearCart(state) {
29 state.items = [];
30 state.total = 0;
31 },
32
33 toggleCart(state) {
34 state.isOpen = !state.isOpen;
35 },
36 },
37});
38
39// RTK auto-generates these - no manual action creators needed
40export const { addItem, removeItem, clearCart, toggleCart } = cartSlice.actions;
41
42// Action types generated: 'cart/addItem', 'cart/removeItem', etc.
43export default cartSlice.reducer;
443. What is createAsyncThunk and how do you use it?
createAsyncThunk is RTK's built-in solution for handling async operations like API calls. It takes care of a pattern you'd otherwise have to build manually every single time: dispatching a 'pending' action when the async operation starts, a 'fulfilled' action with the result when it succeeds, and a 'rejected' action with the error when it fails.
1import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
2
3// Step 1: Create the async thunk
4export const fetchProducts = createAsyncThunk(
5 'products/fetchAll', // action type prefix
6 async (filters, { rejectWithValue }) => {
7 try {
8 const response = await fetch(`/api/products?category=${filters.category}`);
9 if (!response.ok) throw new Error('Server error');
10 return await response.json(); // this becomes action.payload in 'fulfilled'
11 } catch (error) {
12 return rejectWithValue(error.message); // this goes to 'rejected'
13 }
14 }
15);
16
17// Step 2: Handle the lifecycle actions in the slice
18const productsSlice = createSlice({
19 name: 'products',
20 initialState: { list: [], loading: false, error: null },
21 reducers: {},
22 extraReducers: (builder) => {
23 builder
24 .addCase(fetchProducts.pending, (state) => {
25 state.loading = true;
26 state.error = null;
27 })
28 .addCase(fetchProducts.fulfilled, (state, action) => {
29 state.loading = false;
30 state.list = action.payload;
31 })
32 .addCase(fetchProducts.rejected, (state, action) => {
33 state.loading = false;
34 state.error = action.payload;
35 });
36 },
37});
38
39export default productsSlice.reducer;
40
41// Step 3: Dispatch from a component
42function ProductList() {
43 const dispatch = useDispatch();
44 const { list, loading, error } = useSelector(state => state.products);
45
46 useEffect(() => {
47 dispatch(fetchProducts({ category: 'electronics' }));
48 }, []);
49
50 if (loading) return <Spinner />;
51 if (error) return <Error message={error} />;
52 return <ul>{list.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
53}
544. What is middleware in Redux and how does it work?
Middleware sits between the dispatch call and the moment the action reaches the reducer. It intercepts every action and can inspect it, modify it, delay it, stop it, or dispatch additional actions before the original one reaches the reducer. This is how Redux handles async operations, logging, error reporting, and analytics.
A Redux middleware is a curried function with the signature: store → next → action. next is the function that passes the action forward to the next middleware (or the reducer if it's the last one). You can do things before and after calling next.
1// Custom middleware - logs every action + the state change it caused
2const loggerMiddleware = (store) => (next) => (action) => {
3 console.group(`Action: ${action.type}`);
4 console.log('Before state:', store.getState());
5 console.log('Action payload:', action.payload);
6
7 const result = next(action); // pass to next middleware or reducer
8
9 console.log('After state:', store.getState());
10 console.groupEnd();
11
12 return result;
13};
14
15// Custom middleware - track actions to analytics service
16const analyticsMiddleware = (store) => (next) => (action) => {
17 if (action.type === 'cart/addItem') {
18 analytics.track('Product Added', { productId: action.payload.id });
19 }
20 return next(action);
21};
22
23// Register middleware in the store
24const store = configureStore({
25 reducer: rootReducer,
26 middleware: (getDefaultMiddleware) =>
27 getDefaultMiddleware().concat(loggerMiddleware, analyticsMiddleware),
28 // getDefaultMiddleware() includes redux-thunk automatically
29});
305. What is Redux Thunk and why do you need it?
Normally, Redux dispatch only accepts plain action objects. The problem is that real applications need to do async things - fetch from an API, wait for a response, then dispatch the result. You can't put async logic in a reducer (reducers must be pure and synchronous). Redux Thunk solves this by allowing you to dispatch functions instead of just objects.
A thunk is just a function that returns another function. When the Thunk middleware sees you dispatching a function (instead of an object), it calls that function with dispatch and getState as arguments. Inside that function, you can do async work and dispatch regular actions when you're ready. With Redux Toolkit, createAsyncThunk builds on this pattern so you rarely write raw thunks by hand.
1// A manual thunk - function that returns a function
2export const loginUser = (credentials) => async (dispatch, getState) => {
3 dispatch({ type: 'auth/loginStart' }); // loading state on
4
5 try {
6 const response = await fetch('/api/login', {
7 method: 'POST',
8 body: JSON.stringify(credentials),
9 headers: { 'Content-Type': 'application/json' },
10 });
11 const data = await response.json();
12
13 dispatch({ type: 'auth/loginSuccess', payload: data });
14
15 // Can access current state too
16 const currentCart = getState().cart.items;
17 if (currentCart.length > 0) {
18 dispatch(syncCartWithServer(currentCart));
19 }
20
21 } catch (error) {
22 dispatch({ type: 'auth/loginFailed', payload: error.message });
23 }
24};
25
26// Dispatching a thunk from a component - looks the same as dispatching an action
27dispatch(loginUser({ email: 'alice@example.com', password: 'secret' }));
28
29// Redux Thunk middleware intercepts this function dispatch,
30// calls it with (dispatch, getState), and the rest is handled inside the thunk
316. What is Immer and how does Redux Toolkit use it?
One of the most tedious parts of vanilla Redux was writing immutable update logic. Because you can't mutate state directly, you had to spread objects, slice arrays, and nest these operations for deeply nested state. A simple update like changing one nested field could produce 5 lines of spread syntax.
Immer is a library that solves this. It gives you a draft proxy of your state. You write code that looks like you're mutating it directly. Under the hood, Immer tracks what you changed and produces a new immutable state object - you never actually mutated anything. Redux Toolkit builds Immer in by default inside createSlice reducers.
1// ✗ Vanilla Redux - immutable updates by hand (painful for nested state)
2case 'UPDATE_USER_ADDRESS':
3 return {
4 ...state,
5 user: {
6 ...state.user,
7 address: {
8 ...state.user.address,
9 city: action.payload.city,
10 pincode: action.payload.pincode,
11 },
12 },
13 };
14
15// ✅ Redux Toolkit with Immer - write 'mutating' code, get immutable result
16updateAddress(state, action) {
17 state.user.address.city = action.payload.city;
18 state.user.address.pincode = action.payload.pincode;
19 // That's it. Immer handles producing the new state object.
20},
21
22// More examples - all of these look like mutations but are actually safe:
23addItem(state, action) {
24 state.items.push(action.payload); // normally NOT allowed in Redux
25 // With Immer: safe - produces a new array with the item added
26},
27
28toggleItemSelected(state, action) {
29 const item = state.items.find(i => i.id === action.payload);
30 if (item) item.selected = !item.selected; // direct property assignment - safe
31},
32
33removeItem(state, action) {
34 const index = state.items.findIndex(i => i.id === action.payload);
35 if (index !== -1) state.items.splice(index, 1); // splice - normally forbidden
36},
377. What are useSelector and useDispatch hooks?
These two hooks are how your React components connect to the Redux store. useSelector reads data from the store. useDispatch gives you the dispatch function to send actions. They replaced the older connect() higher-order component pattern and made Redux components significantly cleaner.
1import { useSelector, useDispatch } from 'react-redux';
2import { addItem, removeItem, clearCart } from './cartSlice';
3import { selectCartItems, selectCartTotal } from './selectors';
4
5function CartDrawer() {
6 const dispatch = useDispatch();
7
8 // useSelector subscribes this component to the state it selects
9 // Component re-renders ONLY when this specific data changes
10 const items = useSelector(selectCartItems);
11 const total = useSelector(selectCartTotal);
12 const isLoggedIn = useSelector(state => state.auth.isLoggedIn);
13
14 const handleRemove = (itemId) => dispatch(removeItem(itemId));
15 const handleClear = () => dispatch(clearCart());
16
17 return (
18 <div>
19 <p>Total: ₹{total}</p>
20 {items.map(item => (
21 <div key={item.id}>
22 <span>{item.name} × {item.quantity}</span>
23 <button onClick={() => handleRemove(item.id)}>Remove</button>
24 </div>
25 ))}
26 <button onClick={handleClear}>Clear Cart</button>
27 </div>
28 );
29}
30
31// TypeScript typed hooks - create these once and use everywhere
32import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
33import type { RootState, AppDispatch } from './store';
34
35export const useAppDispatch = () => useDispatch<AppDispatch>();
36export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
378. What is state normalisation in Redux and why does it matter?
Normalising state means storing your data in a flat, database-like structure rather than as nested arrays. Instead of an array of objects (where finding or updating one item requires looping), you store entities in an object keyed by their ID. This makes lookups O(1) instead of O(n) and eliminates duplicate data across the store.
1// ✗ UNNORMALISED - nested arrays cause problems
2// To update product 42, you loop through all orders, then all items in each order
3const badState = {
4 orders: [
5 {
6 id: 1,
7 items: [
8 { id: 42, name: 'Laptop', price: 75000 },
9 { id: 43, name: 'Mouse', price: 1500 },
10 ],
11 },
12 ],
13};
14
15// ✅ NORMALISED - flat structure, instant lookups
16const goodState = {
17 products: {
18 ids: [42, 43],
19 entities: {
20 42: { id: 42, name: 'Laptop', price: 75000 },
21 43: { id: 43, name: 'Mouse', price: 1500 },
22 },
23 },
24 orders: {
25 ids: [1],
26 entities: {
27 1: { id: 1, productIds: [42, 43] }, // reference by ID, not duplication
28 },
29 },
30};
31
32// Update product 42 - no looping needed
33goodState.products.entities[42].price = 70000; // direct access
34
35// Redux Toolkit's createEntityAdapter does all this for you automatically
36import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
37
38const productsAdapter = createEntityAdapter();
39// Gives you: selectAll, selectById, selectIds, addOne, addMany,
40// updateOne, removeOne, upsertOne - all built-in
41
42const productsSlice = createSlice({
43 name: 'products',
44 initialState: productsAdapter.getInitialState({ loading: false }),
45 reducers: {
46 productAdded: productsAdapter.addOne,
47 productsLoaded: productsAdapter.setAll,
48 productUpdated: productsAdapter.updateOne,
49 },
50});
519. What are action creators and what problem do they solve?
Action creators are functions that return action objects. They exist to avoid writing the same action object shape everywhere in your code and to prevent typos in action type strings. Instead of writing the full object every time, you call a function.
1// ✗ Without action creators - type strings repeated everywhere
2// A typo in 'cart/addItem' causes silent bugs (no error thrown)
3dispatch({ type: 'cart/addItem', payload: product }); // file A
4dispatch({ type: 'cart/addItm', payload: product }); // file B - typo! silent bug
5dispatch({ type: 'cart/add_item', payload: product }); // file C - different format!
6
7// ✅ With action creators - one definition, used everywhere
8const addItem = (product) => ({ type: 'cart/addItem', payload: product });
9const removeItem = (id) => ({ type: 'cart/removeItem', payload: id });
10
11// Now you can't make a typo - if the function name is wrong, JS throws a ReferenceError
12dispatch(addItem(product)); // clean
13dispatch(removeItem(item.id)); // clean
14
15// With Redux Toolkit's createSlice - action creators are auto-generated
16// You get them directly from slice.actions - no manual writing needed
17import { addItem, removeItem, clearCart } from './cartSlice';
18dispatch(addItem(product));
19dispatch(removeItem(42));
2010. What is combineReducers and when do you use it?
combineReducers is a utility that merges multiple reducer functions into a single root reducer. Each reducer manages its own slice of the state tree. When an action is dispatched, combineReducers sends it to ALL the reducers, but each one only responds to the action types it cares about and ignores the rest.
1import { combineReducers } from '@reduxjs/toolkit';
2import cartReducer from './features/cart/cartSlice';
3import authReducer from './features/auth/authSlice';
4import productsReducer from './features/products/productsSlice';
5import uiReducer from './features/ui/uiSlice';
6
7// Manually (vanilla Redux)
8const rootReducer = combineReducers({
9 cart: cartReducer,
10 auth: authReducer,
11 products: productsReducer,
12 ui: uiReducer,
13});
14
15// With configureStore (RTK) - pass the same object directly
16// configureStore calls combineReducers for you behind the scenes
17const store = configureStore({
18 reducer: {
19 cart: cartReducer,
20 auth: authReducer,
21 products: productsReducer,
22 ui: uiReducer,
23 },
24});
25
26// Result: state tree structure mirrors the reducer keys:
27// {
28// cart: { items: [], total: 0 },
29// auth: { user: null, isLoggedIn: false },
30// products: { list: [], loading: false },
31// ui: { isSidebarOpen: false, theme: 'light' }
32// }
33III. Advanced Level
1. What is Redux Saga and how does it differ from Redux Thunk?
Both Thunk and Saga handle async side effects in Redux - but they operate very differently and are suited to different levels of complexity. Thunk is simple: dispatch a function, do async work inside, dispatch results. Saga is powerful: use JavaScript generator functions to model complex async workflows as readable, testable sequences.
| Feature | Redux Thunk | Redux Saga |
|---|---|---|
| Mechanism | Functions (async/await) | Generator functions (function*) |
| Complexity | Low - easy to learn and use | High - generators + effects API |
| Testability | Requires mocking API calls | Excellent - test effect descriptions, not actual execution |
| Cancellation | Manual - AbortController or flags | Built-in - take, cancel, race effects |
| Best for | Simple to moderate async flows (fetch and dispatch) | Complex workflows: polling, optimistic updates, race conditions, long-running tasks |
1// THUNK - simple async action
2export const fetchUser = (id) => async (dispatch) => {
3 dispatch(setLoading(true));
4 try {
5 const user = await api.getUser(id);
6 dispatch(setUser(user));
7 } catch (err) {
8 dispatch(setError(err.message));
9 } finally {
10 dispatch(setLoading(false));
11 }
12};
13
14// SAGA - same thing but with generators
15import { call, put, takeLatest } from 'redux-saga/effects';
16
17function* fetchUserSaga(action) {
18 yield put(setLoading(true));
19 try {
20 const user = yield call(api.getUser, action.payload); // pauseable call
21 yield put(setUser(user));
22 } catch (err) {
23 yield put(setError(err.message));
24 } finally {
25 yield put(setLoading(false));
26 }
27}
28
29// Saga's superpower: takeLatest cancels previous in-flight requests
30// If the user types fast and 5 fetch requests fire, only the last one matters
31function* watchFetchUser() {
32 yield takeLatest('user/fetchUser', fetchUserSaga);
33}
342. What is RTK Query and how does it change data fetching in Redux?
RTK Query is a powerful data fetching and caching tool built directly into Redux Toolkit. It's designed to eliminate the repetitive work of writing loading states, error handling, and cache invalidation for every single API endpoint. Think of it as React Query but deeply integrated with Redux.
Without RTK Query, adding one API endpoint to Redux requires: a createAsyncThunk, loading/error state in the slice, extraReducers for pending/fulfilled/rejected, a selector, and then calling it in a component with useEffect. That's 30+ lines for a single GET request. RTK Query collapses all of that into a few lines.
1import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
2
3// Define your API - one file handles all endpoints
4export const productsApi = createApi({
5 reducerPath: 'productsApi',
6 baseQuery: fetchBaseQuery({
7 baseUrl: '/api',
8 prepareHeaders: (headers, { getState }) => {
9 const token = getState().auth.token;
10 if (token) headers.set('Authorization', `Bearer ${token}`);
11 return headers;
12 },
13 }),
14 tagTypes: ['Product', 'Cart'], // for cache invalidation
15 endpoints: (builder) => ({
16 getProducts: builder.query({
17 query: (filters) => `products?category=${filters.category}`,
18 providesTags: ['Product'],
19 }),
20 getProductById: builder.query({
21 query: (id) => `products/${id}`,
22 }),
23 addToCart: builder.mutation({
24 query: (item) => ({ url: 'cart', method: 'POST', body: item }),
25 invalidatesTags: ['Cart'], // auto-refetch cart after mutation
26 }),
27 updateProduct: builder.mutation({
28 query: ({ id, ...patch }) => ({ url: `products/${id}`, method: 'PATCH', body: patch }),
29 invalidatesTags: ['Product'],
30 }),
31 }),
32});
33
34// RTK Query auto-generates these hooks
35export const {
36 useGetProductsQuery,
37 useGetProductByIdQuery,
38 useAddToCartMutation,
39 useUpdateProductMutation,
40} = productsApi;
41
42// Usage in a component - loading, error, data all handled automatically
43function ProductList({ category }) {
44 const { data, isLoading, isError, error } = useGetProductsQuery({ category });
45
46 if (isLoading) return <Spinner />;
47 if (isError) return <Error message={error.message} />;
48
49 return data.map(p => <ProductCard key={p.id} product={p} />);
50}
513. What is Reselect and why do you need memoized selectors?
useSelector runs your selector function every time any state in the store changes - not just the state you care about. If your selector does an expensive computation (filtering a large list, mapping and transforming data, calculating derived values), it re-runs on every single state update. This is a real performance problem in large apps.
Reselect solves this with memoization. You create a selector using createSelector with input selectors and a result function. Reselect caches the result of the last call. If the inputs haven't changed, it returns the cached result without running the expensive computation again.
1import { createSelector } from '@reduxjs/toolkit'; // re-exported from Reselect
2
3// Base selectors - simple, cheap
4const selectAllProducts = state => state.products.list;
5const selectActiveFilter = state => state.filters.category;
6const selectPriceRange = state => state.filters.priceRange;
7const selectSearchQuery = state => state.filters.searchQuery;
8
9// ✗ Without Reselect - this heavy computation runs on EVERY state change
10const selectFilteredProducts = state => {
11 return state.products.list
12 .filter(p => p.category === state.filters.category)
13 .filter(p => p.price >= state.filters.priceRange[0] && p.price <= state.filters.priceRange[1])
14 .filter(p => p.name.toLowerCase().includes(state.filters.searchQuery.toLowerCase()))
15 .sort((a, b) => a.price - b.price);
16};
17// If you have 10,000 products, this runs on every single action - even unrelated ones
18
19// ✅ With createSelector - result is memoized
20export const selectFilteredProducts = createSelector(
21 [selectAllProducts, selectActiveFilter, selectPriceRange, selectSearchQuery],
22 // This result function ONLY runs when any of the inputs above actually change
23 (products, category, [minPrice, maxPrice], query) => {
24 return products
25 .filter(p => p.category === category)
26 .filter(p => p.price >= minPrice && p.price <= maxPrice)
27 .filter(p => p.name.toLowerCase().includes(query.toLowerCase()))
28 .sort((a, b) => a.price - b.price);
29 }
30);
31
32// Now if only auth state changes, this selector returns the cached result
33// The expensive filter + sort only re-runs when products, filter, or query changes
344. What are common Redux performance pitfalls and how do you fix them?
Redux is fast out of the box, but it's easy to write code that causes unnecessary re-renders and slows down your app. Here are the pitfalls you'll actually encounter in production codebases and how to fix each one.
| Pitfall | What happens | Fix |
|---|---|---|
| Inline selector creates new reference | useSelector(state => ({ a: state.a, b: state.b })) returns a new object every time → always re-renders | Use createSelector (Reselect) or separate useSelector calls for each value |
| Expensive computation in selector | Heavy filtering or mapping runs on every state change, not just relevant ones | Memoize with createSelector |
| Storing non-serialisable data | Storing Dates, Promises, class instances, or functions in Redux breaks DevTools and breaks assumptions | Store only plain, serialisable values - ISO strings for dates, IDs for references |
| Too much in Redux | Storing UI state like 'is modal open', tooltip hover state, or form draft values in Redux creates unnecessary noise | Keep UI-only state local with useState. Use Redux for shared, persistent, or async state. |
| Unnormalised nested arrays | Finding and updating items in deeply nested arrays requires looping and spread nesting | Normalise with createEntityAdapter - O(1) lookups and updates |
1// ✗ Returns new object every render - component always re-renders
2const data = useSelector(state => ({
3 user: state.auth.user,
4 items: state.cart.items,
5}));
6
7// ✅ Separate selectors - only re-renders when specific value changes
8const user = useSelector(selectCurrentUser);
9const items = useSelector(selectCartItems);
10
11// ✅ Or use shallowEqual to compare objects field-by-field
12import { shallowEqual } from 'react-redux';
13const { user, items } = useSelector(
14 state => ({ user: state.auth.user, items: state.cart.items }),
15 shallowEqual // prevents re-render if values are the same
16);
175. What is the Redux DevTools extension and how does time-travel debugging work?
Redux DevTools is a browser extension that gives you a live view of your Redux store. It shows every action that has been dispatched, the state before and after each action, and the exact diff between them. It is one of the biggest reasons teams choose Redux - debugging becomes vastly easier when you can see exactly what changed and why.
Action log: See every dispatched action in chronological order. Click any action to inspect its type and payload.
State inspector: See the full state tree at any point in time. Browse the tree with filtering.
Diff view: See exactly which parts of state changed between two consecutive actions - fields added, removed, or updated highlighted clearly.
Time-travel debugging: Drag the slider back in time to replay your session from any point. Click 'Jump' on any action to teleport the application state back to that moment. No page refresh, no re-running your scenario.
Import/Export: Save the full action history as a JSON file and share it with a colleague who can replay the exact same session on their machine to reproduce a bug.
Time-travel works because Redux reducers are pure functions. Given the same state and the same action, they always produce the same next state. This means Redux can replay any sequence of actions from the beginning and arrive at exactly the same state every time. The DevTools exploits this determinism to let you rewind and replay.
6. How do you persist Redux state across page refreshes?
By default, Redux state lives in memory and is lost the moment the user refreshes the page. For things like a shopping cart, user preferences, or authentication tokens, you want that state to survive refreshes. The standard solution is redux-persist.
1import { configureStore } from '@reduxjs/toolkit';
2import { persistStore, persistReducer, FLUSH, REHYDRATE, PAUSE,
3 PERSIST, PURGE, REGISTER } from 'redux-persist';
4import storage from 'redux-persist/lib/storage'; // localStorage
5import sessionStorage from 'redux-persist/lib/storage/session'; // sessionStorage
6
7import cartReducer from './cartSlice';
8import authReducer from './authSlice';
9import uiReducer from './uiSlice';
10
11// Configure which slices to persist and where
12const cartPersistConfig = {
13 key: 'cart',
14 storage, // localStorage - survives tab close
15 whitelist: ['items', 'total'], // only persist these fields
16};
17
18const authPersistConfig = {
19 key: 'auth',
20 storage: sessionStorage, // sessionStorage - cleared on tab close
21 blacklist: ['isLoading'], // don't persist loading state
22};
23
24export const store = configureStore({
25 reducer: {
26 cart: persistReducer(cartPersistConfig, cartReducer),
27 auth: persistReducer(authPersistConfig, authReducer),
28 ui: uiReducer, // not persisted - always fresh
29 },
30 middleware: (getDefaultMiddleware) =>
31 getDefaultMiddleware({
32 serializableCheck: {
33 // redux-persist uses non-serialisable values internally - ignore them
34 ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
35 },
36 }),
37});
38
39export const persistor = persistStore(store);
40
41// Wrap your app with PersistGate to delay rendering until state is rehydrated
42import { PersistGate } from 'redux-persist/integration/react';
43
44function App() {
45 return (
46 <Provider store={store}>
47 <PersistGate loading={<Spinner />} persistor={persistor}>
48 <Router />
49 </PersistGate>
50 </Provider>
51 );
52}
537. What are the best practices for structuring a large Redux codebase?
Structuring Redux at scale is where junior developers often get lost. The official Redux Toolkit recommendation is the 'feature folder' (also called ducks pattern) - co-locate everything related to a feature in one folder. Here's what that looks like in practice.
1src/
2├── app/
3│ ├── store.ts // configureStore - single source of truth
4│ ├── hooks.ts // typed useAppSelector, useAppDispatch
5│ └── rootReducer.ts // combineReducers if needed separately
6│
7├── features/
8│ ├── auth/
9│ │ ├── authSlice.ts // reducer + actions + initial state
10│ │ ├── authSelectors.ts // all selectors for auth
11│ │ ├── authThunks.ts // createAsyncThunk calls
12│ │ ├── AuthPage.tsx // the UI
13│ │ └── authApi.ts // API call functions
14│ │
15│ ├── cart/
16│ │ ├── cartSlice.ts
17│ │ ├── cartSelectors.ts
18│ │ ├── CartDrawer.tsx
19│ │ └── CartItem.tsx
20│ │
21│ └── products/
22│ ├── productsSlice.ts
23│ ├── productsSelectors.ts
24│ ├── productsApi.ts // or RTK Query apiSlice
25│ └── ProductList.tsx
26│
27└── api/
28 └── apiSlice.ts // RTK Query base API definition
29Co-locate by feature, not by type: Put the slice, selectors, thunks, and UI in the same folder. 'cart/cartSlice.ts' is better than 'reducers/cart.ts'.
Separate selectors: Even one file, even small ones - putting selectors in their own file prevents circular imports and makes them easy to memoize with Reselect.
Typed hooks: Create useAppSelector and useAppDispatch once in app/hooks.ts and import from there - never from react-redux directly. This gives TypeScript full inference everywhere.
Only global state in Redux: Form state, modal open/close, tooltip visibility - these are local state. Put them in useState. Redux is for state that multiple unrelated features need.
8. How does Redux compare to Zustand and Jotai in modern React apps?
The state management landscape has changed significantly. Zustand and Jotai have gained a lot of popularity because they solve similar problems with dramatically less setup. Knowing the trade-offs shows a senior-level understanding of the ecosystem.
| Feature | Redux (RTK) | Zustand | Jotai |
|---|---|---|---|
| Model | Single centralised store | One or many stores | Atomic - independent atoms of state |
| Boilerplate | Medium (RTK reduced it a lot) | Very low | Very low |
| DevTools | Excellent - Redux DevTools | Good - integrates with Redux DevTools | Basic - Jotai DevTools plugin |
| Data fetching | Built-in RTK Query | Manual or React Query | Manual or React Query |
| Best for | Large enterprise apps, teams needing strict patterns, complex async | Medium apps, teams wanting Redux-like but simpler | Apps with many independent state values, React Suspense |
Honest take: for a new small-to-medium project, Zustand will get you to the same place as Redux with about 1/3 of the code. Choose Redux when your team is large and consistency matters, when you need RTK Query's powerful caching, when you need the DevTools for complex debugging, or when you're building something that could grow very complex over time. Don't choose a state management library based on hype - choose it based on what your project actually needs.
Related Articles
React JS
Prepare for your React interview with the most asked questions for freshers and experienced developers. Covers hooks, lifecycle, performance optimization, and real-world scenarios.
FrontendJavaScript
Prepare for your next tech interview with the most asked JavaScript interview questions and answers. It includes basic to advanced concepts, coding problems, and real-world scenarios for freshers and experienced developers.