Introduction
Lesson 5.11 taught Redux's ideas — one store, actions, pure reducers. Written the old way, those ideas drowned in boilerplate: action-type constants, switch statements, manual immutable updates, store setup plumbing. Redux Toolkit (RTK) is the official modern way: the same ideas, with the ceremony removed — and the package the Redux team tells every new app to use. (Roman Urdu: Redux Toolkit wohi Redux hai, bas purani lambi formality khatam — kam code, wohi nizam.)
Key points
- Setup is two moves:
configureStore({ reducer: { cart: cartReducer } })builds the store (with DevTools and good defaults built in), and the app is wrapped in<Provider store={store}>— Context, working quietly underneath. createSlicewrites three things at once: give it aname, aninitialStateandreducers, and it generates the reducer and the action creators for you.cartSlice.actions.addItemexists without you declaring a single action type.- Reducers may "mutate" — safely: RTK bundles Immer, so inside a slice reducer you write
state.items.push(item)and Immer produces the correct immutable copy behind the scenes. (Outside RTK this remains a bug — the permission belongs to Immer only.)(Roman Urdu: Slice ke andar push likh sakte ho — Immer peeche asal copy khud bana deta hai.) - Components read with
useSelector:const items = useSelector((state) => state.cart.items);— pick your slice's piece; the component re-renders when that piece changes. - Components change with
useDispatch:const dispatch = useDispatch();thendispatch(addItem(product))— the same dispatch verb as always, now carrying Toolkit-generated actions. - File habit: one slice per file (
cartSlice.js,userSlice.js), each exporting its reducer as default and its actions by name — the structure every modern Redux codebase shares.
Code examples
Example 1 — The cart slice: a whole feature in one small file
// cartSlice.js
import { createSlice } from "@reduxjs/toolkit";
const cartSlice = createSlice({
name: "cart",
initialState: { items: [] },
reducers: {
addItem(state, action) {
state.items.push(action.payload);
},
removeItem(state, action) {
state.items = state.items.filter((item) => item.id !== action.payload);
},
clearCart(state) {
state.items = [];
},
},
});
export const { addItem, removeItem, clearCart } = cartSlice.actions;
export default cartSlice.reducer;What it does: One createSlice call replaces the old trio of action constants, action creators and a switch reducer. The reducers read like direct edits — push, filter, clear — while Immer guarantees the store still receives fresh immutable states. The generated actions come out at the bottom, named and ready.
What you will see: nothing visual yet — this file is the engine. Its whole API is three verbs: addItem(product), removeItem(id), clearCart().
Example 2 — The store and the Provider
// store.js
import { configureStore } from "@reduxjs/toolkit";
import cartReducer from "./cartSlice";
export const store = configureStore({
reducer: {
cart: cartReducer,
},
});
// main.jsx
import { Provider } from "react-redux";
import { store } from "./store";
root.render(
<Provider store={store}>
<App />
</Provider>
);What it does: configureStore assembles the slices into one state tree (state.cart…), and the Provider — the Context wrapper from react-redux — makes that store reachable from every component. Two small files and the app's shared state has a home.
What you will see: in Redux DevTools, a state tree shaped exactly like the configuration: { cart: { items: [] } }, ready to fill.
Example 3 — Reading and dispatching: badge and buttons
import { useSelector, useDispatch } from "react-redux";
import { addItem, removeItem } from "./cartSlice";
function Shop() {
const items = useSelector((state) => state.cart.items);
const dispatch = useDispatch();
const total = items.reduce((sum, i) => sum + i.price, 0);
return (
<div>
<p>Cart: {items.length} items · Rs {total}</p>
<button onClick={() => dispatch(addItem({ id: 1, name: "Chai", price: 250 }))}>
Add Chai
</button>
<ul>
{items.map((item, idx) => (
<li key={idx}>
{item.name} — Rs {item.price}
<button onClick={() => dispatch(removeItem(item.id))}>Remove</button>
</li>
))}
</ul>
</div>
);
}What it does: useSelector subscribes the component to the cart slice; dispatch sends the generated actions. The total is derived during render, never stored. Every part of the 5.11 triangle is present — with a fraction of the old code.
What you will see: each "Add Chai" grows the list and total together; Remove deletes its row; Redux DevTools shows the cart/addItem actions marching in order, replayable one by one.
Quick recap
- RTK = Redux without ceremony:
configureStore+<Provider>for the store,createSlicegenerating reducer + actions together. - Inside slice reducers, Immer lets you write pushes and assignments while the store still gets immutable updates.
- Components live on
useSelector(read a slice) anduseDispatch(send a generated action).
Common mistakes
- Mutating state outside Immer's reach — treating a selector result as editable, or mutating inside a plain hand-written reducer, breaks Redux's change detection. The fix: "mutation style" is legal only inside
createSlicereducers (and only on the draft state they receive). - Selecting the whole state —
useSelector(state => state)re-renders the component on every action anywhere in the app. The fix: select the smallest piece the component actually shows. - Dispatching during render — calling
dispatchin the component body fires actions mid-render and loops. The fix: dispatch lives in event handlers and effects, exactly like setState discipline.
Practice
- Add a
userSlicebeside the cart: state{ name: "", loggedIn: false }, reducerslogin(takes the name as payload) andlogout. Register it in the store, and build a small header that shows "Hello, {name}" or a Login button by selecting the user slice. - Give the cart slice quantities:
addItemshould raiseqtywhen the id is already in the cart instead of adding a duplicate row. Update the Shop list to showname × qtyand make the total useprice * qty.