Chapter 12 - Let's Build Our Store (Redux Complete)
Chapter 12 - Let's Build Our Store (Redux Complete)
Hey everyone! Welcome back to Namaste React!
Today we build the complete cart system for our food delivery app! Multiple slices, memoized selectors, Redux DevTools, middleware — the full production-grade Redux setup!
What we will cover:
- Multiple Redux slices — cart, user, restaurants
- Computed/derived state with createSelector
- Redux DevTools — time-travel debugging
- Custom middleware in RTK
- Real-world Redux folder structure
- Optimistic updates
- RTK Query — data fetching with Redux
- Interview Questions
1. Multiple Slices — Cart, User, UI
// src/utils/cartSlice.js — Full cart with quantities
import { createSlice } from "@reduxjs/toolkit";
const cartSlice = createSlice({
name: "cart",
initialState: {
items: [], // { id, name, price, quantity, imageId }
promoCode: null,
discount: 0,
},
reducers: {
addItem: (state, action) => {
const existingItem = state.items.find(i => i.id === action.payload.id);
if (existingItem) {
existingItem.quantity += 1; // Immer mutation — safe!
} else {
state.items.push({ ...action.payload, quantity: 1 });
}
},
removeItem: (state, action) => {
const item = state.items.find(i => i.id === action.payload.id);
if (item) {
if (item.quantity > 1) {
item.quantity -= 1;
} else {
state.items = state.items.filter(i => i.id !== action.payload.id);
}
}
},
clearCart: (state) => {
state.items = [];
state.promoCode = null;
state.discount = 0;
},
applyPromo: (state, action) => {
const { code, discount } = action.payload;
state.promoCode = code;
state.discount = discount;
},
},
});
export const { addItem, removeItem, clearCart, applyPromo } = cartSlice.actions;
export default cartSlice.reducer;
// src/utils/userSlice.js
import { createSlice } from "@reduxjs/toolkit";
const userSlice = createSlice({
name: "user",
initialState: {
name: null,
email: null,
address: null,
isLoggedIn: false,
token: null,
},
reducers: {
setUser: (state, action) => {
const { name, email, address, token } = action.payload;
state.name = name;
state.email = email;
state.address = address;
state.token = token;
state.isLoggedIn = true;
},
clearUser: (state) => {
state.name = null;
state.email = null;
state.address = null;
state.token = null;
state.isLoggedIn = false;
},
updateAddress: (state, action) => {
state.address = action.payload;
},
},
});
export const { setUser, clearUser, updateAddress } = userSlice.actions;
export default userSlice.reducer;
// src/utils/appStore.js — Combined store
import { configureStore } from "@reduxjs/toolkit";
import cartReducer from "./cartSlice";
import userReducer from "./userSlice";
const appStore = configureStore({
reducer: {
cart: cartReducer,
user: userReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loggerMiddleware),
});
export default appStore;
2. Derived State with createSelector (Reselect)
// WITHOUT memoized selector — recomputes on every render:
function Cart() {
const items = useSelector(state => state.cart.items);
const discount = useSelector(state => state.cart.discount);
// This runs on every render, even if items didn't change:
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const discountedTotal = total - (total * discount / 100);
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
}
// WITH createSelector — memoized! Only recalculates when inputs change:
import { createSelector } from "@reduxjs/toolkit";
// Input selectors:
const selectCartItems = state => state.cart.items;
const selectDiscount = state => state.cart.discount;
// Memoized derived selectors:
export const selectCartTotal = createSelector(
[selectCartItems],
(items) => items.reduce((sum, item) => sum + item.price * item.quantity, 0)
// Only recomputes when items array changes!
);
export const selectCartItemCount = createSelector(
[selectCartItems],
(items) => items.reduce((sum, item) => sum + item.quantity, 0)
);
export const selectDiscountedTotal = createSelector(
[selectCartTotal, selectDiscount],
(total, discount) => total - (total * discount / 100)
);
// Usage — clean components:
function Cart() {
const items = useSelector(selectCartItems);
const total = useSelector(selectCartTotal);
const discountedTotal = useSelector(selectDiscountedTotal);
const itemCount = useSelector(selectCartItemCount);
return (
<div>
<h2>Cart ({itemCount} items)</h2>
<p>Subtotal: ₹{total}</p>
<p>Total after discount: ₹{discountedTotal}</p>
</div>
);
}
// selectCartTotal only recalculates when items change.
// If user state changes or unrelated state changes → uses cached value!
3. Redux DevTools — Time Travel Debugging
HOW TO USE:
============
1. Install Chrome extension: "Redux DevTools"
2. Open your React app with Redux
3. Open DevTools (F12) → "Redux" tab
WHAT YOU CAN SEE:
==================
├── Action Log (left panel)
│ ├── cart/addItem ← Every action dispatched
│ ├── cart/addItem
│ └── cart/removeItem
│
├── State (right panel)
│ └── Current state tree
│
└── Diff (right panel)
└── What changed in state
TIME TRAVEL:
=============
Click any past action → jump back to that state!
See EXACTLY what state was at any point.
Find bugs by replaying actions step by step.
CONFIGURING DevTools:
======================
// configureStore enables DevTools automatically in development!
// In production, DevTools is disabled automatically.
const store = configureStore({
reducer: { cart: cartReducer },
devTools: process.env.NODE_ENV !== "production", // manual control
});
4. Custom Middleware
// Middleware sits BETWEEN dispatch and the reducer.
// Perfect for: logging, analytics, authentication checks, async logic.
// LOGGING MIDDLEWARE:
const loggerMiddleware = store => next => action => {
console.group(`ACTION: ${action.type}`);
console.log("Before state:", store.getState());
console.log("Action:", action);
const result = next(action); // ← pass to next middleware or reducer
console.log("After state:", store.getState());
console.groupEnd();
return result;
};
// ANALYTICS MIDDLEWARE:
const analyticsMiddleware = store => next => action => {
if (action.type === "cart/addItem") {
// Send to Google Analytics
window.gtag("event", "add_to_cart", {
item_name: action.payload.name,
price: action.payload.price,
});
}
return next(action);
};
// Add to store:
const store = configureStore({
reducer: { cart: cartReducer },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
.concat(loggerMiddleware)
.concat(analyticsMiddleware),
});
5. Optimistic Updates
// Optimistic update: Update UI immediately, then sync with server.
// If server fails, roll back.
const cartSlice = createSlice({
name: "cart",
initialState: { items: [], previousItems: null },
reducers: {
// Optimistic add:
addItemOptimistic: (state, action) => {
state.previousItems = [...state.items]; // save rollback point
state.items.push({ ...action.payload, quantity: 1 });
},
// Roll back if server fails:
rollbackAdd: (state) => {
if (state.previousItems) {
state.items = state.previousItems;
state.previousItems = null;
}
},
confirmAdd: (state) => {
state.previousItems = null; // clear rollback point on success
},
},
});
// Usage:
function AddToCartButton({ item }) {
const dispatch = useDispatch();
const handleAdd = async () => {
dispatch(addItemOptimistic(item)); // ← UI updates IMMEDIATELY
try {
await api.addToCart(item); // ← sync with server
dispatch(confirmAdd());
} catch (error) {
dispatch(rollbackAdd()); // ← revert if server fails
alert("Failed to add item. Please try again.");
}
};
return <button onClick={handleAdd}>Add to Cart</button>;
}
// User feels zero delay — UI updates instantly!
// Server sync happens in the background!
6. Real-World Redux Folder Structure
src/ ├── app/ │ └── store.js ← configureStore, combine all reducers │ ├── features/ ← Feature-based organization (modern approach) │ ├── cart/ │ │ ├── cartSlice.js ← reducer + actions + selectors for cart │ │ ├── Cart.jsx ← Cart UI component │ │ └── cartSelectors.js ← createSelector memoized selectors │ │ │ ├── user/ │ │ ├── userSlice.js │ │ └── UserProfile.jsx │ │ │ └── restaurants/ │ ├── restaurantsSlice.js │ ├── RestaurantList.jsx │ └── restaurantSelectors.js │ └── main.jsx ← Provider wraps the App
7. RTK Query — Data Fetching with Redux
// RTK Query: API caching + data fetching built into Redux Toolkit!
// Eliminates the need for separate useEffect + useState for every API call.
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
// Define the API:
export const restaurantApi = createApi({
reducerPath: "restaurantApi",
baseQuery: fetchBaseQuery({ baseUrl: "https://api.example.com/" }),
endpoints: (builder) => ({
getRestaurants: builder.query({
query: ({ lat, lng }) => `restaurants?lat=${lat}&lng=${lng}`,
}),
getRestaurantMenu: builder.query({
query: (resId) => `restaurant/${resId}/menu`,
}),
}),
});
// Auto-generated hooks!
export const { useGetRestaurantsQuery, useGetRestaurantMenuQuery } = restaurantApi;
// Add to store:
const store = configureStore({
reducer: {
cart: cartReducer,
[restaurantApi.reducerPath]: restaurantApi.reducer, // ← add RTK Query reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(restaurantApi.middleware), // ← add middleware
});
// Usage in components — no useEffect needed!
function Body() {
const { data, isLoading, error } = useGetRestaurantsQuery({ lat: 12.97, lng: 77.59 });
if (isLoading) return <Shimmer />;
if (error) return <p>Error loading restaurants!</p>;
return <RestaurantList items={data} />;
}
// RTK Query handles:
// ✅ Loading states (isLoading, isFetching)
// ✅ Error states (error, isError)
// ✅ Caching (same query = cached result, no duplicate fetches!)
// ✅ Automatic refetching (on window focus, reconnect)
// ✅ Cache invalidation (tags system)
Interview Questions
Q: What is createSelector and why use it?
"createSelector from Redux Toolkit (via Reselect) creates memoized selector functions. It takes input selectors and a result function. The result function only re-runs when the input selector results change. This prevents expensive computations (like array reduce) from running on every render when unrelated state changes. Essential for computed state like cart totals, filtered lists, and derived values."
Q: What is Redux middleware?
"Middleware sits between dispatching an action and the reducer receiving it. It's a function with the signature store => next => action => next(action). Common uses: logging (log every action and state change), analytics (send events to tracking), authentication (check tokens before certain actions), and async handling (redux-thunk, which RTK includes by default). You add middleware in configureStore's middleware option."
Q: What is RTK Query?
"RTK Query is a powerful data fetching and caching solution built into Redux Toolkit. You define API endpoints with createApi and get auto-generated hooks like useGetRestaurantsQuery. It handles loading/error states, caching (same query returns cached data without new fetch), automatic refetching, and cache invalidation. It eliminates most useEffect + useState patterns for data fetching."
Q: What is an optimistic update in Redux?
"An optimistic update immediately applies a state change to the UI before the server confirms it, then rolls back if the server call fails. This makes the app feel instant — the user sees feedback immediately without waiting for the network. You save a rollback state before the optimistic update, call the API, and either confirm (clear rollback) or revert (restore rollback) based on the response."
Key Points to Remember
| Concept | Key Takeaway |
|---|---|
| Multiple slices | One slice per feature domain: cartSlice, userSlice, restaurantsSlice. |
| createSelector | Memoized derived state. Only recomputes when input selectors' results change. |
| Redux DevTools | See all actions, state at each step, diff changes, time-travel. Enabled by default in dev. |
| Middleware | store → next → action pipeline. Intercept for logging, analytics, auth checks. |
| Optimistic update | Update UI first, sync server after. Roll back on failure. Makes app feel instant. |
| RTK Query | Built-in data fetching. Auto-hooks, caching, loading/error states. Replaces fetch + useEffect. |
| Feature structure | src/features/cart/ — co-locate slice, components, selectors per domain. |
What's Next?
In Chapter 13, we learn how to test our React app with React Testing Library and Jest. Tests are what separate production-ready code from hobby projects!
Keep coding, keep learning! See you in the next one!
Post a Comment