Chapter 11 - Data is the New Oil (Redux Toolkit)
Chapter 11 - Data is the New Oil (Redux Toolkit)
Hey everyone! Welcome back to Namaste React!
As our app grows, state management becomes the hardest problem. Today we solve it with Redux Toolkit — the modern, official way to write Redux. No boilerplate, pure power!
What we will cover:
- The state management problem at scale
- Context API limitations
- Redux core concepts — Store, Action, Reducer, Dispatch
- Redux Toolkit — createSlice, configureStore
- Provider, useSelector, useDispatch
- Async actions with createAsyncThunk
- Redux DevTools
- Context vs Redux — when to use which
- Interview Questions
1. The State Management Problem at Scale
SMALL APP — useState + props works fine:
=========================================
<App>
/ \
<Header> <Body>
|
<Cart> ← App passes cartItems down through Header
Simple. Manageable. Fine.
LARGE APP — state lives in many places:
=========================================
<App>
/ | \
<Header> <Body> <Checkout>
| | |
<Cart> <ItemList> <OrderSummary>
| |
<ItemCard> <CartItems>
"Add to cart" clicked in <ItemCard>
→ cartItems state needs to update
→ Header shows cart count
→ Checkout shows items
→ CartItems shows items
Without global state: cartItems lives in App and drills through EVERY level.
Change App's state structure → update 15 components!
This is PROP DRILLING HELL.
THE SOLUTION — Global State Store:
====================================
REDUX STORE
{ cartItems: [] }
/ | \
Header Body Checkout ← Any component reads directly!
| ← Any component writes directly!
(shows count) ← No prop drilling!
ANY component can READ from store (useSelector)
ANY component can WRITE to store (useDispatch)
State change → ALL subscribers automatically update!
2. Context API Limitations
CONTEXT API WORKS BUT HAS PROBLEMS:
=====================================
1. PERFORMANCE — All consumers re-render when context changes.
If you put { user, cart, theme, settings } in one context,
changing cart re-renders ALL components using that context.
Including those that only care about user!
2. NO DEV TOOLS — No way to inspect state changes over time.
No time-travel debugging. Hard to debug.
3. NO MIDDLEWARE — Can't intercept actions for logging, analytics, etc.
4. MESSY FOR COMPLEX LOGIC — Managing complex state transitions
with multiple contexts + useReducer gets unwieldy.
5. NO CACHING — Context doesn't handle async data fetching,
loading states, or caching out of the box.
WHEN IS CONTEXT OK?
→ Theme (dark/light) — changes rarely
→ Current logged-in user — changes rarely
→ Locale / language — changes rarely
→ Simple, infrequently changing global state
WHEN USE REDUX:
→ Cart items, complex state that many components read/write
→ Large teams that need predictable state management
→ When you need DevTools and time-travel debugging
→ When state transitions have business logic
3. Redux Core Concepts
┌─────────────────────────────────────────────────────────────┐
│ REDUX FLOW │
└─────────────────────────────────────────────────────────────┘
USER CLICKS "Add to Cart"
↓
dispatch(addItem(pizza)) ← Action dispatched
↓
Redux Store receives action
↓
Reducer runs: (state, action) → newState
↓
Store's state UPDATES
↓
useSelector subscribers notified
↓
Cart icon re-renders with new count!
CORE CONCEPTS:
===============
1. STORE — Single object holding ALL application state
{ cart: { items: [], total: 0 }, user: { name: "Akshay" }, ... }
2. ACTION — Plain object describing WHAT happened
{ type: "cart/addItem", payload: { id: 1, name: "Pizza", price: 299 } }
3. REDUCER — Pure function: (currentState, action) → newState
Takes current state + action → returns new state. NO side effects!
4. DISPATCH — Send an action to the store
dispatch({ type: "cart/addItem", payload: pizza })
5. SELECTOR — Function to read a specific piece of state
const items = useSelector(state => state.cart.items)
4. Redux Toolkit — Setup
// Install Redux Toolkit + React-Redux:
npm install @reduxjs/toolkit react-redux
// STEP 1: Create a slice (combines actions + reducer):
// src/utils/cartSlice.js
import { createSlice } from "@reduxjs/toolkit";
const cartSlice = createSlice({
name: "cart", // ← slice name (prefixes action types)
initialState: {
items: [],
},
reducers: {
// Each key becomes an ACTION CREATOR and handles that action in the reducer
addItem: (state, action) => {
// ✅ In RTK, you CAN "mutate" state directly!
// (Immer.js handles creating a new immutable state under the hood)
state.items.push(action.payload);
},
removeItem: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload.id);
},
clearCart: (state) => {
state.items = [];
},
},
});
// Auto-generated action creators:
export const { addItem, removeItem, clearCart } = cartSlice.actions;
// Export the reducer:
export default cartSlice.reducer;
// STEP 2: Create the Store:
// src/utils/appStore.js
import { configureStore } from "@reduxjs/toolkit";
import cartReducer from "./cartSlice";
import userReducer from "./userSlice";
const appStore = configureStore({
reducer: {
cart: cartReducer, // state.cart
user: userReducer, // state.user
},
});
export default appStore;
// STEP 3: Provide the store to React:
// src/main.jsx
import { Provider } from "react-redux";
import appStore from "./utils/appStore";
ReactDOM.createRoot(document.getElementById("root")).render(
<Provider store={appStore}>
<App />
</Provider>
);
5. Reading and Writing State — useSelector and useDispatch
// READING state — useSelector:
import { useSelector } from "react-redux";
function Header() {
// Reads cart.items from the store
const cartItems = useSelector(state => state.cart.items);
return (
<header>
<span>Cart ({cartItems.length})</span>
</header>
);
}
// Header ONLY re-renders when cart.items changes!
// If user state changes → Header does NOT re-render (it doesn't select user)
// WRITING state — useDispatch:
import { useDispatch } from "react-redux";
import { addItem, removeItem } from "../utils/cartSlice";
function RestaurantCard({ item }) {
const dispatch = useDispatch();
const handleAddToCart = () => {
dispatch(addItem(item)); // ← dispatch the action creator
};
return (
<div>
<h3>{item.name}</h3>
<p>₹{item.price}</p>
<button onClick={handleAddToCart}>Add to Cart +</button>
</div>
);
}
// Cart page — reading and modifying:
function Cart() {
const cartItems = useSelector(state => state.cart.items);
const dispatch = useDispatch();
const total = cartItems.reduce((sum, item) => sum + item.price, 0);
return (
<div>
{cartItems.length === 0
? <p>Cart is empty. Add some items!</p>
: (
<>
{cartItems.map(item => (
<div key={item.id}>
<span>{item.name} — ₹{item.price}</span>
<button onClick={() => dispatch(removeItem(item))}>Remove</button>
</div>
))}
<p>Total: ₹{total}</p>
<button onClick={() => dispatch(clearCart())}>Clear Cart</button>
</>
)
}
</div>
);
}
6. Why You Can "Mutate" State in RTK — Immer.js
THE CONFUSING PART:
====================
// In vanilla Redux — NEVER mutate! Always return new state:
function reducer(state = [], action) {
switch (action.type) {
case "ADD_ITEM":
return [...state, action.payload]; // ← new array!
case "REMOVE_ITEM":
return state.filter(item => item.id !== action.payload.id);
}
}
// In RTK — you CAN "mutate":
addItem: (state, action) => {
state.items.push(action.payload); // ← looks like mutation!
}
WHY IS THIS OK IN RTK?
========================
RTK uses IMMER.JS under the hood.
Immer creates a "draft" (proxy) of the state.
You mutate the draft.
Immer produces a NEW immutable state from your mutations.
The original state is NEVER actually mutated!
// What actually happens:
addItem: (state, action) => {
// 'state' is actually a Proxy (Immer draft)
state.items.push(action.payload);
// Immer: "You pushed to the draft. Let me create a NEW state with this change."
// Returns: { ...state, items: [...state.items, action.payload] }
}
// You get the BEST of both worlds:
// ✅ Simple mutation syntax (no spreading, no [...state])
// ✅ Immutable state (Immer ensures no direct mutation)
// BUT: You must either mutate OR return a new value. Not both!
// ✅ Mutate: state.items.push(x)
// ✅ Return: return { ...state, items: [] }
// ❌ Both: state.items.push(x); return { ...state } ← Error!
7. Async Actions with createAsyncThunk
// For async operations (API calls) in Redux:
import { createAsyncThunk } from "@reduxjs/toolkit";
// STEP 1: Create the thunk:
export const fetchRestaurants = createAsyncThunk(
"restaurants/fetchAll", // ← action type prefix
async ({ lat, lng }, thunkAPI) => {
const response = await fetch(
`https://api.swiggy.com/restaurants?lat=${lat}&lng=${lng}`
);
const data = await response.json();
return data.restaurants; // ← becomes action.payload on success
}
);
// STEP 2: Handle in slice with extraReducers:
const restaurantSlice = createSlice({
name: "restaurants",
initialState: { items: [], loading: false, error: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchRestaurants.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchRestaurants.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchRestaurants.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
}
});
// STEP 3: Dispatch in a component:
function Body() {
const dispatch = useDispatch();
const { items, loading, error } = useSelector(state => state.restaurants);
useEffect(() => {
dispatch(fetchRestaurants({ lat: 12.97, lng: 77.59 }));
}, []);
if (loading) return <Shimmer />;
if (error) return <p>Error: {error}</p>;
return <RestaurantList items={items} />;
}
Interview Questions
Q: What is Redux and why do we need it?
"Redux is a state management library that provides a single, centralized store for all application state. Any component can read from it (useSelector) or write to it (useDispatch) without prop drilling. We need it when useState and Context become unwieldy — when many unrelated components need the same state, when state transitions have complex business logic, or when we need powerful DevTools for debugging."
Q: What is Redux Toolkit?
"Redux Toolkit is the official, recommended way to write Redux. It eliminates the boilerplate of vanilla Redux by providing createSlice (combines actions and reducers), configureStore (sets up store with DevTools and middleware), and createAsyncThunk (for async actions). It also includes Immer.js so you can write 'mutating' code that's actually immutable under the hood."
Q: What is a Reducer in Redux?
"A reducer is a pure function that takes the current state and an action, and returns a new state. (currentState, action) => newState. It must be pure — no side effects, no API calls, no random values. Given the same inputs, it always returns the same output. This makes state changes predictable and testable."
Q: What is the difference between useSelector and useDispatch?
"useSelector reads data from the Redux store — it takes a selector function that receives the entire state and returns the piece you need. The component re-renders when that piece changes. useDispatch returns the store's dispatch function, which you call with an action to trigger a state change. Reading is useSelector, writing is useDispatch."
Q: Why does RTK allow direct state mutation in reducers?
"RTK uses Immer.js under the hood. The state parameter in RTK reducers is actually an Immer 'draft' proxy, not the real state. When you 'mutate' it, Immer intercepts those mutations and produces a new immutable state. The actual Redux state is never mutated. You get the developer convenience of mutation syntax while maintaining Redux's immutability guarantee."
Key Points to Remember
| Concept | Key Takeaway |
|---|---|
| Redux Store | Single source of truth for all app state. Provided to React via Provider. |
| Action | Plain object: { type, payload }. Describes what happened. |
| Reducer | Pure function: (state, action) → newState. No side effects. |
| createSlice | Combines actions + reducer. Auto-generates action creators. RTK's core API. |
| configureStore | Creates store with DevTools + middleware. Combines reducers. |
| useSelector | Read from store. Component re-renders only when selected data changes. |
| useDispatch | Write to store. dispatch(actionCreator(payload)) triggers state update. |
| Immer.js | Lets you "mutate" in reducers. Draft proxy → new immutable state under the hood. |
| createAsyncThunk | For async operations. Dispatches pending/fulfilled/rejected actions automatically. |
What's Next?
In Chapter 12, we build the complete cart system from scratch using Redux Toolkit, including multiple slices, memoized selectors, and the full checkout flow!
Keep coding, keep learning! See you in the next one!
Post a Comment