What you’ll learn
- ●What is Redux?
- ●Store, Actions & Reducers
- One-way data flowFree account
- Why Redux Toolkit?Free account
- configureStore + ProviderFree account
- createSliceFree account
- useSelector & useDispatchFree account
- Immer & immutabilityFree account
- createAsyncThunk (async data)Free account
- What is RTK Query?Free account
- RTK Query: queries & cachingFree account
- RTK Query: mutations & invalidationFree account
- Middleware & DevToolsFree account
- Interview recapFree account
- ●Project 1: Counter with Redux Toolkit
- Project 2: Todo app with Redux ToolkitFree account
- Project 3: Posts app with RTK QueryFree account
What is Redux?
Imagine a busy office where everyone needs to know the same numbers — today's sales, who is on leave, the current target. If each person keeps their own paper note and passes it hand-to-hand, notes get lost, two people end up with different numbers, and nobody knows which is correct. Instead, the office puts one big whiteboard on the wall. Everyone reads from it, and when something changes, one person updates the board — now the whole office sees the same truth.
Redux works exactly like this. In a React app, many components need the same data — the logged-in user, the shopping cart, a dark-mode flag. Passing it down through props from parent to child to grandchild (called prop-drilling) gets painful: middle components carry data they do not even use, and one change means editing many files. Redux puts all that shared state in one central store — a single whiteboard every component can read from and write to directly.
🌍 Real-world example: On Zomato, your cart must be visible on the menu page, the checkout page, and the top bar at the same time. Instead of threading the cart through every screen, the app keeps it in one store; every screen reads the same cart, and adding an item updates it everywhere at once.
💡 Global/shared state = data that many parts of the app need, not just one component. 💡 Prop-drilling = passing data through many middle components that do not use it, just to reach a deep child. 💡 Store = the single object that holds your whole app's shared state — one source of truth.
With modern Redux Toolkit (RTK) — the official, recommended way to use Redux today — creating this store is a one-liner. Because everyone reads and writes to the same store, the data can never disagree with itself, and debugging is easy: there is only one place to look.
Standard definition: Redux is a predictable state-management library that stores an application's shared state in a single central store, giving all components one source of truth to read from and update.
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from './cartSlice';
// One central store = one source of truth
export const store = configureStore({
reducer: { cart: cartReducer },
});
console.log(store.getState());Store, Actions & Reducers
Think of a bank. Your account balance is kept safely in one place — you cannot just walk in and change the number yourself. To change it you fill a slip (deposit ₹500 / withdraw ₹200) and hand it to the teller, who reads the slip and updates your balance by the rules.
Redux works exactly like this with 3 core pieces:
- Store = the account balance. One single place that holds all app state. You never edit it directly.
- Action = the slip. A plain object describing what happened:
{ type: 'account/deposit', payload: 500 }.typeis a label;payloadis the data. - Reducer = the teller. A pure function
(state, action) => newStatethat reads the slip and returns the new state — no side effects, no changing the old state, same input always gives same output.
How they relate: you dispatch an action (hand over the slip) → the reducer runs and computes the new state → the store updates and tells the UI to re-render.
🌍 Real-world example: In a Zomato cart, tapping "Add" dispatches
{ type: 'cart/add', payload: item }. The reducer returns a new cart with that item, the store updates, and the cart badge shows the new count.
💡 Store = single source of truth that holds the whole app's state. 💡 Action = a plain
{type, payload}object describing an event that happened. 💡 Reducer = a pure function that takes the old state + an action and returns the new state. 💡 Pure function = no side effects; same inputs always produce the same output.
Standard definition: In Redux, the store holds the single state tree, actions are plain objects describing what happened, and reducers are pure functions that take the current state and an action and return the next state. (With Redux Toolkit you write these via configureStore and createSlice, which auto-generate actions and reducers for you.)
const deposit = amt => ({ type: 'account/deposit', payload: amt });
function reducer(state, action) {
if (action.type === 'account/deposit')
return { balance: state.balance + action.payload };
return state;
}
reducer({ balance: 100 }, deposit(500)); // { balance: 600 }Project 1: Counter with Redux Toolkit
What we're building: the same tiny Counter — a number on screen with +, −, Reset, and a +5 button — but this time the number does not live inside the component. It lives in a Redux store, and the component only reads it and sends messages to change it. This one project teaches the entire Redux Toolkit loop end-to-end: button click → dispatch(action) → reducer runs → new state in the store → useSelector re-renders → UI updates. Master this loop and every Redux app you ever see is just a bigger version of it.
What it teaches: createSlice (reducers + auto-generated actions), configureStore, <Provider>, useSelector (read), useDispatch (write), and payloads (incrementByAmount(5)).
Step 0 — The mental model (read this first, 30 seconds)
In the plain React Counter, the number lived inside the component via useState. The component owned it. That's perfect for one small component — but the moment ten different components across your app need the same number (a navbar cart badge, a checkout page, a mini-cart popup), passing it down through props gets painful.
Redux flips the ownership. The number does not belong to the component anymore — it lives in a single shared box called the store, sitting outside the whole component tree. The component becomes a thin screen: it reads the number from the store, and when you click a button it doesn't change the number directly — it dispatches an action ("please increment"), and a pure function called a reducer computes the new number inside the store. The store then tells every subscribed component "the value changed, re-read it."
💡 Store = one shared box holding your app's state, living outside every component. Action = a plain message describing what happened (
{ type: 'counter/increment' }). Reducer = a pure function that takes the current state + an action and returns the new state. dispatch = the only way to send an action to the store.
🌍 Real-world example: On Zomato, your cart item-count shows in the top bar, on the restaurant page, and in the checkout summary — all at once. Nobody passes that number through fifty props. It lives in one store; every component just reads it and dispatches
addItem/removeItem. Our counter is that exact pattern shrunk to one number.
Keep this picture in your head: the component doesn't own the number — the store does. Now let's build.
Step 1 — Create the slice (the heart of it)
A slice is one feature's whole Redux logic in a single file: its starting state, and all the ways it can change. We create it with createSlice.
// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
reset: (state) => {
state.value = 0;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, reset, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
What's happening — read each piece:
name: 'counter'— a label for this slice. It becomes the prefix of every action type, soincrementreally dispatches'counter/increment'. This is what you'd have hand-written as an action-type constant in old Redux — RTK generates it for you.initialState: { value: 0 }— the state this slice starts with. We wrap the number in an object ({ value: 0 }) because slice state is normally an object with fields.reducers: { ... }— each function here is a mini-reducer describing one kind of change.incrementbumps the value up,decrementdown,resetzeroes it,incrementByAmountadds whatever number you pass.- "Wait,
state.value += 1is mutation!" Yes — and it's allowed here.createSliceruns your reducer inside Immer, which records your "mutations" and safely produces a brand-new immutable state behind the scenes. So you write easy code and still get immutable updates. (This mutation freedom exists only inside createSlice — never mutate Redux state anywhere else.) counterSlice.actions— RTK auto-generated an action creator for every reducer.incrementis now a function: callingincrement()returns the action object{ type: 'counter/increment' }. We export them so components can dispatch them.counterSlice.reducer— the one combined reducer function for this slice. The store needs this. We export it as the default.
💡 createSlice = one call that gives you three things: the reducer, the auto-generated action creators, and (via Immer) the freedom to write mutating-looking code safely.
Step 2 — Create the store and wrap the app in <Provider>
The slice knows how state changes, but it isn't running anywhere yet. configureStore boots up the actual store, and <Provider> makes it reachable from every component.
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
// main.jsx
import { Provider } from 'react-redux';
import { store } from './store';
import Counter from './Counter';
function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
export default App;
What's happening:
configureStore({ reducer: { counter: counterReducer } })builds the single store. Thereducerobject maps a slice name → its reducer. Socounter: counterReducermeans "the store has acounterbranch, managed by our counter slice." State now lives atstate.counter.value. (If you later add acartslice, you'd addcart: cartReducerright beside it.)configureStorealso hands you a lot for free: Redux DevTools, the thunk middleware for async, and dev-only checks that scream if you accidentally mutate state outside Immer. No manual setup needed.<Provider store={store}>wraps the app. This is the wire that connects Redux to React: any component inside this Provider can now useuseSelectoranduseDispatchto reach the store. Forget the Provider and every hook throws "could not find react-redux context."
💡 Provider = the bridge that plugs the store into the React tree. Everything inside it can read from and dispatch to the store; everything outside it can't.
Step 3 — Read the value with useSelector, get dispatch with useDispatch
Now the Counter component. First, wire it to read from the store and get the ability to write.
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, reset, incrementByAmount } from './counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<h2>Counter</h2>
<p>Count: {count}</p>
</div>
);
}
export default Counter;
What's happening:
useSelector((state) => state.counter.value)— this is how a component reads from the store. You hand it a selector: a function that takes the whole store state and returns just the slice you care about. Here we digstate.counter.value(remember:counteris the branch name from the store,valueis the field frominitialState).countis now0.- The magic of
useSelector: it subscribes the component to exactly that value. Ifstate.counter.valuechanges, this component automatically re-runs and re-renders. If some other part of the store changes, this component is left alone — no wasted renders. const dispatch = useDispatch()— this gives you thedispatchfunction, the only way to send actions to the store. We don't call any action yet; we're just grabbing the tool. Next step wires it to buttons.
💡 useSelector = read a specific slice of store state and subscribe to it. useDispatch = get the
dispatchfunction so you can send actions.
Step 4 — Wire the buttons and trace the full loop
Now the buttons. Each one dispatches an action creator. This is the whole point — trace it slowly.
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<h2>Counter</h2>
<p>Count: {count}</p>
<button onClick={() => dispatch(decrement())}>-</button>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(reset())}>Reset</button>
</div>
);
}
The full loop — click the + button and follow every beat. Every button in every Redux app works exactly this way:
- You click the
+button. - Its
onClickrunsdispatch(increment()). increment()runs first — it's the auto-generated action creator, so it returns the action object{ type: 'counter/increment' }.dispatch(...)sends that action into the store.- The store runs its reducer. It sees
type: 'counter/increment'and routes it to your slice'sincrementreducer. - Inside
increment,state.value += 1runs. Immer records that and produces a new state wherevaluewent from0to1. The store swaps in this new state. - The store notifies its subscribers that state changed.
- Your
useSelector((state) => state.counter.value)re-reads and now returns1— a different value from before — so React re-renders the Counter. - The JSX produces
Count: 1. You see it on screen.
Notice what you did NOT do: you never touched count directly (it's read-only — try count = 5 and nothing happens), and you never touched the DOM. You only dispatched an action. The reducer changed the store; the store re-rendering you is automatic. Say it out loud: the component doesn't change the number — it dispatches, the reducer changes the store, the store re-renders the component.
− and Reset are the identical loop with different actions: dispatch(decrement()) routes to the decrement reducer (value -= 1), dispatch(reset()) routes to reset (value = 0). Same nine beats every time.
Step 5 — incrementByAmount(5): passing a payload
So far the actions carried no data — "increment" is enough by itself. But often an action needs to carry a value: add how much? That extra data is the payload.
<button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
Trace the payload flow:
- Click +5 →
onClickrunsdispatch(incrementByAmount(5)). incrementByAmount(5)— the auto-generated action creator takes your argument and packs it into the action'spayloadfield, returning{ type: 'counter/incrementByAmount', payload: 5 }.dispatchsends it to the store; the reducer routes it to yourincrementByAmountreducer, which received(state, action).- Inside,
state.value += action.payloadruns —action.payloadis5, so if value was1it becomes6. - Store swaps in new state →
useSelectorre-reads6→ re-render → screen showsCount: 6.
That's the whole reason a reducer's second argument is action: it's how the data you passed rides along to the reducer. increment ignored action because it needed no data; incrementByAmount reads action.payload because it does.
💡 payload = the data an action carries.
incrementByAmount(5)→action.payloadis5inside the reducer. Whatever you pass to the action creator becomes the payload.
🔎 The full flow (recap the wiring)
Here's the finished app, click by click:
- Page loads →
<Provider store={store}>makes the store available →Counterruns →useSelectorreadsstate.counter.value=0→ screen showsCount: 0. - Click + →
dispatch(increment())→ action{type:'counter/increment'}hits the store →incrementreducer runsstate.value += 1→ Immer makes new state (value1) → store notifies →useSelectorre-reads1→ re-render →Count: 1. - Click − →
dispatch(decrement())→decrementreducer runsstate.value -= 1→ new state → re-read → re-render → number goes down. - Click Reset →
dispatch(reset())→resetreducer setsvalue = 0→ new state → re-render →Count: 0. - Click +5 →
dispatch(incrementByAmount(5))→ action{type:'counter/incrementByAmount', payload:5}→ reducer runsstate.value += action.payload→ new state (+5) → re-render.
Every single button follows the same beats: click → dispatch(action) → reducer runs → new state in store → useSelector re-renders → UI updates. You never mutated count, never touched the DOM — you only ever dispatched actions.
✅ What you just learned
- The store owns the state, not the component — the number lives in the Redux store; the component just reads and dispatches.
createSlice— one call gives you the reducer, auto-generated action creators (increment,incrementByAmount, …), and Immer's safe "mutating" syntax inside reducers.configureStore+<Provider>—configureStore({ reducer: { counter: counterReducer } })builds the store;<Provider store={store}>bridges it into the React tree so hooks work.useSelector— read a slice of state and subscribe to it, so the component re-renders only when that value changes.useDispatch— getdispatch, the only way to send an action to the store.- payload — data an action carries;
incrementByAmount(5)→action.payloadis5. - The universal Redux loop — click → dispatch → reducer → new state → useSelector re-render → UI update. Every Redux app is built on this one loop.
⚠️ Common mistakes
- Forgetting
<Provider>— no Provider meansuseSelector/useDispatchcan't find the store and throw "could not find react-redux context." Wrap the app once at the top. - Mutating state outside a slice —
state.value += 1is fine insidecreateSlice(Immer), but writing to Redux state anywhere else breaks everything. Outside slices, treat state as read-only. - Selecting the whole state —
useSelector((state) => state)subscribes you to every change in the store, so the component re-renders on unrelated updates. Always select the narrowest thing you need:state.counter.value. - Dispatching the creator without calling it —
dispatch(increment)(no()) dispatches the function itself, not an action. It must bedispatch(increment()). - Trying to change state by assigning to
count—count = 5does nothing;countis a read-only value fromuseSelector. The only way to change store state is todispatchan action.
// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; },
reset: (state) => { state.value = 0; },
incrementByAmount: (state, action) => { state.value += action.payload; },
},
});
export const { increment, decrement, reset, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({ reducer: { counter: counterReducer } });
// Counter.jsx
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, reset, incrementByAmount } from './counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<h2>Counter</h2>
<p>Count: {count}</p>
<button onClick={() => dispatch(decrement())}>-</button>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
<button onClick={() => dispatch(reset())}>Reset</button>
</div>
);
}
export default Counter;
// main.jsx
import { Provider } from 'react-redux';
import { store } from './store';
export default function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}Redux Toolkitinterview questions & answers
10 sample questions below — 174+ in the full bank inside.
What are the three lifecycle states of a createAsyncThunk?
pending, fulfilled, and rejected. pending fires the moment the thunk starts (before the await finishes), fulfilled fires when the async function returns successfully with its return value as payload, and rejected fires if it throws or the promise fails, carrying the error.
In simple terms: It's like a courier tracking status: 'shipped' the moment it leaves (pending), 'delivered' when it arrives (fulfilled), or 'returned to sender' if something breaks (rejected). Example: fetchUsers.pending, fetchUsers.fulfilled, and fetchUsers.rejected are the three action types RTK auto-creates.
How do you trigger a createAsyncThunk from a component?
You get dispatch from useDispatch() and call dispatch(thunkName(arg)) — usually inside a useEffect for initial loads or in an event handler. Dispatching returns a promise, and passing an argument (like an id) becomes the thunk's first payload parameter.
In simple terms: It's like pressing a 'Refresh' button that kicks off the whole fetch-and-store process. Example: const dispatch = useDispatch(); useEffect(() => { dispatch(fetchUsers()); }, [dispatch]); — this runs the thunk once when the component mounts.
In a createAsyncThunk, where does the fulfilled action's payload come from?
Whatever value the async payload-creator function returns becomes action.payload in the fulfilled case. So if your function does return res.json(), that parsed data lands in action.payload inside the fulfilled reducer.
In simple terms: Think of the thunk like a delivery guy: whatever he hands you at the door (the return value) is exactly what ends up in your hands (action.payload). Example: async () => { const res = await fetch('/api/todos'); return res.json(); } → in fulfilled, action.payload is the todos array.
Should a Redux app have one store or many?
A Redux app should have exactly one store — it's the single source of truth for the whole app's state. You don't create multiple stores; instead you split state into multiple slices and combine their reducers inside that one configureStore call. One store, many slices.
In simple terms: Think of one store like a single shared whiteboard for the whole team — everyone reads and writes to the same board, not separate private ones. Example: cart, user, and orders are three slices, but all live in one store created by a single configureStore({ reducer: { cart, user, orders } }).
What is createAsyncThunk in Redux Toolkit?
createAsyncThunk is an RTK helper that wraps an async function (like an API call) and automatically generates a thunk action creator plus three lifecycle action types: pending, fulfilled, and rejected. You dispatch it, and it dispatches those three actions for you as the promise runs, so you just handle them in your slice.
In simple terms: Think of it like ordering food on Zomato: as soon as you place the order you get a 'preparing' update (pending), then either 'delivered' (fulfilled) or 'order failed' (rejected). createAsyncThunk sends those three status updates automatically. Example: const fetchUsers = createAsyncThunk('users/fetch', async () => { const res = await fetch('/api/users'); return res.json(); }).
What is the first argument to createAsyncThunk used for?
The first argument is a string type prefix, like 'users/fetchUsers'. RTK uses it to generate the three action type strings: 'users/fetchUsers/pending', '.../fulfilled', and '.../rejected'. It's mainly a label so you can identify the thunk in Redux DevTools.
In simple terms: It's like naming a WhatsApp group — the name doesn't do the work, it just lets everyone recognise which chat it is. Example: createAsyncThunk('posts/fetchPosts', async () => …) produces action types starting with 'posts/fetchPosts/'.
What is configureStore in Redux Toolkit?
configureStore is the modern RTK function that creates the Redux store. You pass it a `reducer` object mapping slice names to their reducers, and it returns a fully set-up store with thunk middleware, Redux DevTools, and dev-only immutability/serializability checks already wired in.
In simple terms: Think of configureStore like ordering a fully-furnished flat instead of an empty one — furniture (DevTools, middleware, checks) already installed, you just move in. Example: const store = configureStore({ reducer: { counter: counterReducer } }) — one line and your store is ready.
Why do we wrap the app in <Provider store={store}>?
Provider is a React-Redux component that makes the Redux store available to every component in the tree using React Context. You wrap it once near the root and pass the store as a prop; after that any component can call useSelector or useDispatch to talk to the store without prop-drilling.
In simple terms: Think of Provider like the building's main water connection — connect it once at the top and every flat gets water from its own tap. Example: <Provider store={store}><App /></Provider> means any button deep inside App can dispatch actions without passing store down manually.
Why do we use configureStore instead of the old createStore?
createStore is the legacy API that needs manual setup — you combine reducers with combineReducers, apply middleware and DevTools yourself. configureStore does all of that with sane defaults out of the box, so it's the officially recommended way to create a store today. createStore is now deprecated.
In simple terms: createStore is like assembling furniture yourself with 20 screws; configureStore is the same furniture delivered pre-assembled. Example: old way needed createStore(rootReducer, applyMiddleware(thunk)) plus DevTools glue; RTK is just configureStore({ reducer }) — done.
What is createSlice in Redux Toolkit?
createSlice is the core RTK function that bundles a slice of state together in one call: you give it a name, an initialState, and a reducers object, and it returns auto-generated action creators plus a single reducer function. It replaces the old boilerplate of writing action-type constants, action creators, and a switch reducer by hand.
In simple terms: Think of createSlice like a kitchen combo appliance — instead of buying a separate mixer, grinder, and juicer, you get all three in one box. Example: const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { state.value++ } } }) gives you both counterSlice.actions.increment and counterSlice.reducer at once.
164+ more Redux Toolkit questions inside
Create a free account to read the full question bank, learn every topic, and practise with an AI mock interview.
Unlock all questions — freeReady to practise Redux Toolkit?
Unlock every topic free, then face an AI interviewer that asks follow-ups and grades your answers.