Chapter 09 - Optimizing Our App
Chapter 09 - Optimizing Our App
Hey everyone! Welcome back to Namaste React!
Today we make our app FAST! We learn Custom Hooks, useMemo, useCallback, React.memo, useReducer, and useContext. These are the tools that separate a good React developer from a great one!
What we will cover:
- Custom Hooks — extracting and reusing logic
- useMemo — memoizing expensive calculations
- useCallback — memoizing functions
- React.memo — preventing child re-renders
- When to optimize (and when NOT to!)
- useReducer — complex state management
- useContext — sharing state without prop drilling
- Interview Questions
1. Custom Hooks — Extracting and Reusing Logic
WHAT IS A CUSTOM HOOK? ======================= A custom hook is a JavaScript function that: 1. Starts with "use" (required! React enforces this convention) 2. Can call other hooks inside it 3. Extracts reusable stateful logic from components Custom hooks are NOT a React API feature. They're just a pattern — a convention for sharing logic!
EXAMPLE 1: useOnlineStatus — is the user online?
==================================================
// Without custom hook — duplicated logic in every component:
function Header() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handler = () => setIsOnline(navigator.onLine);
window.addEventListener("online", handler);
window.addEventListener("offline", handler);
return () => {
window.removeEventListener("online", handler);
window.removeEventListener("offline", handler);
};
}, []);
// ...render...
}
// The same useEffect logic repeated in Footer, Body, etc.!
// WITH custom hook — write once, use everywhere:
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
return isOnline; // ← return the value!
}
// Now use it anywhere:
function Header() {
const isOnline = useOnlineStatus(); // One line!
return (
<header>
<span>{isOnline ? "🟢 Online" : "🔴 Offline"}</span>
</header>
);
}
EXAMPLE 2: useFetch — reusable data fetching:
===============================================
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const json = await response.json();
setData(json);
setError(null);
} catch (err) {
if (err.name !== "AbortError") setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
return () => controller.abort();
}, [url]); // re-fetch when URL changes!
return { data, loading, error };
}
// Usage — dead simple!
function RestaurantMenu({ resId }) {
const { data, loading, error } = useFetch(
`https://api.example.com/restaurants/${resId}/menu`
);
if (loading) return <Shimmer />;
if (error) return <ErrorMessage message={error} />;
return <MenuList items={data.menu} />;
}
// The same hook works for ANY URL — restaurants, users, orders, etc.!
EXAMPLE 3: useRestaurantMenu — domain-specific hook:
======================================================
// Move all restaurant menu logic OUT of the component:
function useRestaurantMenu(resId) {
const [restaurantInfo, setRestaurantInfo] = useState(null);
useEffect(() => {
fetchMenu();
}, [resId]);
const fetchMenu = async () => {
const data = await fetch(MENU_API_URL + resId);
const json = await data.json();
setRestaurantInfo(json?.data);
};
return restaurantInfo;
}
// Component is now SUPER clean — just UI!
const RestaurantMenu = () => {
const { resId } = useParams();
const restaurantInfo = useRestaurantMenu(resId);
if (!restaurantInfo) return <Shimmer />;
return (
<div>
<h1>{restaurantInfo?.cards?.[2]?.card?.card?.info?.name}</h1>
{/* ... */}
</div>
);
};
// Logic lives in the hook. UI lives in the component.
// This is the principle of SEPARATION OF CONCERNS!
2. useMemo — Memoizing Expensive Calculations
THE PROBLEM — Expensive computation runs on EVERY render:
==========================================================
function ProductList({ products, filter }) {
// This runs on EVERY render — even if products and filter haven't changed!
const expensiveResult = products
.filter(p => p.category === filter)
.map(p => ({ ...p, discountedPrice: p.price * 0.9 }))
.sort((a, b) => a.discountedPrice - b.discountedPrice);
// If products has 10,000 items, this is SLOW!
// And it runs even when the user just types in an unrelated input!
return <ul>{expensiveResult.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
useMemo — only recompute when dependencies change:
===================================================
import { useMemo } from "react";
function ProductList({ products, filter }) {
const expensiveResult = useMemo(() => {
// Only runs when 'products' or 'filter' changes!
return products
.filter(p => p.category === filter)
.map(p => ({ ...p, discountedPrice: p.price * 0.9 }))
.sort((a, b) => a.discountedPrice - b.discountedPrice);
}, [products, filter]); // ← dependencies
return <ul>{expensiveResult.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
// If the user types in a DIFFERENT input that re-renders ProductList,
// useMemo returns the CACHED result — no recalculation!
// Only recalculates when products or filter actually change!
3. useCallback — Memoizing Functions
THE PROBLEM — New function reference on every render:
=====================================================
function Parent() {
const [count, setCount] = useState(0);
// ❌ New function created on EVERY render!
const handleSubmit = () => {
submitOrder(count);
};
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
<ExpensiveChild onSubmit={handleSubmit} />
</div>
);
}
// ExpensiveChild is wrapped in React.memo:
const ExpensiveChild = React.memo(({ onSubmit }) => {
console.log("ExpensiveChild rendered!");
return <button onClick={onSubmit}>Submit</button>;
});
// Problem: Parent re-renders when count changes.
// handleSubmit is a NEW function on every render.
// React.memo does shallow comparison: new function ≠ old function.
// So ExpensiveChild ALSO re-renders — even though it didn't need to!
useCallback — stable function reference:
=========================================
import { useCallback } from "react";
function Parent() {
const [count, setCount] = useState(0);
// ✅ Same function reference as long as dependencies don't change
const handleSubmit = useCallback(() => {
submitOrder(count);
}, [count]); // ← only new function when count changes
return (
<div>
<ExpensiveChild onSubmit={handleSubmit} />
</div>
);
}
// Now React.memo works correctly!
// handleSubmit reference stays the same → ExpensiveChild doesn't re-render!
4. React.memo — Preventing Child Re-renders
DEFAULT BEHAVIOR — child re-renders whenever parent re-renders:
================================================================
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<p>Parent count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
<Child /> {/* Re-renders EVERY time Parent re-renders! */}
</div>
);
}
function Child() {
console.log("Child rendered!"); // Logs on every parent re-render
return <div>I am child</div>;
}
React.memo — skip re-render if props haven't changed:
=======================================================
const Child = React.memo(function Child() {
console.log("Child rendered!"); // Only logs when props change!
return <div>I am child</div>;
});
// Child receives no props from Parent.
// React.memo sees: "same props (none) as before → skip re-render!" ✅
// With props — shallow comparison:
const RestaurantCard = React.memo(({ name, rating }) => {
console.log("Card rendered:", name);
return (
<div>
<h3>{name}</h3>
<p>{rating}⭐</p>
</div>
);
});
// Card only re-renders if name OR rating changes (shallow comparison).
// If the parent re-renders but name and rating stay the same → no re-render!
// Custom comparison:
const Card = React.memo(
({ user }) => <div>{user.name}</div>,
(prevProps, nextProps) => {
// Return TRUE to SKIP re-render (opposite of shouldComponentUpdate!)
return prevProps.user.id === nextProps.user.id;
}
);
5. When to Optimize — and When NOT To!
THE GOLDEN RULE: MEASURE FIRST, OPTIMIZE SECOND. ================================================= WRONG approach: "I should add useMemo and React.memo everywhere to be safe!" RIGHT approach: "I profiled my app and found THIS specific component is slow. Let me add memoization here." useMemo and useCallback have a COST! - Memory to store the cached value - Comparison logic on every render - More complex code to understand For cheap operations (string concat, simple math): ❌ DON'T useMemo — the overhead is MORE than the savings! WHEN TO USE useMemo: ✅ Filtering/sorting arrays with 1000+ items ✅ Complex math computations (fibonacci, heavy algorithms) ✅ Derived data that expensive to compute ✅ Referential stability for useEffect dependencies WHEN TO USE useCallback: ✅ Function passed to React.memo-wrapped child ✅ Function used as useEffect dependency ✅ Function passed down multiple levels (to prevent cascading re-renders) WHEN TO USE React.memo: ✅ Component renders often with the SAME props ✅ Component renders expensive output (large lists, complex UI) ✅ Pure display components in a frequently-updating parent DON'T ADD THESE FOR: ❌ Components that rarely render ❌ Cheap/fast computations ❌ Components that ALWAYS receive new props (memo never helps!)
6. useReducer — Complex State Management
// useState is great for simple state.
// When state logic gets complex, useReducer is better!
// PROBLEM with multiple useState calls for related state:
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [page, setPage] = useState(1);
const [filters, setFilters] = useState({});
// Hard to ensure consistency — what if loading=true and error is set at the same time?
// SOLUTION: useReducer — all state transitions in one place:
const initialState = {
loading: false,
data: null,
error: null,
page: 1,
};
function reducer(state, action) {
switch (action.type) {
case "FETCH_START":
return { ...state, loading: true, error: null };
case "FETCH_SUCCESS":
return { ...state, loading: false, data: action.payload, error: null };
case "FETCH_ERROR":
return { ...state, loading: false, error: action.payload };
case "NEXT_PAGE":
return { ...state, page: state.page + 1 };
default:
return state;
}
}
function DataComponent() {
const [state, dispatch] = useReducer(reducer, initialState);
const fetchData = async () => {
dispatch({ type: "FETCH_START" }); // ← dispatch an action
try {
const data = await api.fetch();
dispatch({ type: "FETCH_SUCCESS", payload: data });
} catch (err) {
dispatch({ type: "FETCH_ERROR", payload: err.message });
}
};
if (state.loading) return <Spinner />;
if (state.error) return <Error msg={state.error} />;
return <DataList items={state.data} />;
}
useState vs useReducer:
========================
Feature useState useReducer
────────────────────────────────────────────────────────
State type Simple values Complex objects with transitions
Logic location Scattered in handlers Centralized in reducer
Testing Tests call handlers Tests call reducer directly (pure!)
Related state Multiple useState Single state object
Next state depends Functional update form Built into reducer pattern
on previous
Redux familiarity Not applicable Same pattern as Redux!
7. useContext — State Without Prop Drilling
// Context provides a way to pass data through the component tree
// without passing props at every level.
// STEP 1: Create a Context
import { createContext, useContext } from "react";
export const UserContext = createContext(null);
// createContext takes the DEFAULT value (used when no Provider above)
// STEP 2: Provide the context value (wrap your component tree)
function App() {
const [user, setUser] = useState({ name: "Akshay", isLoggedIn: true });
return (
<UserContext.Provider value={{ user, setUser }}>
<AppLayout /> {/* All children can access user! */}
</UserContext.Provider>
);
}
// STEP 3: Consume in any child — NO PROPS NEEDED!
function Header() {
const { user } = useContext(UserContext); // ← read from context!
return <div>Hello, {user.name}!</div>;
}
function LoginButton() {
const { user, setUser } = useContext(UserContext);
return (
<button onClick={() => setUser({ ...user, isLoggedIn: false })}>
Logout
</button>
);
}
// Header and LoginButton can both read/update user
// WITHOUT any prop drilling through AppLayout, Main, Nav, etc.!
// MULTIPLE CONTEXTS — compose them:
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={user}>
<CartContext.Provider value={cart}>
<App />
</CartContext.Provider>
</UserContext.Provider>
</ThemeContext.Provider>
// Performance note: All consumers of a context re-render when the
// context value changes! Split contexts if some consumers only
// need part of the data.
Interview Questions
Q: What is a Custom Hook?
"A custom hook is a JavaScript function whose name starts with 'use' and that can call other hooks internally. It's a way to extract stateful logic from components into reusable functions. Custom hooks are not a React API feature — they're a pattern. They let you share logic like data fetching, form validation, or window resize detection across multiple components without duplicating code."
Q: What is the difference between useMemo and useCallback?
"useMemo memoizes the RESULT of a function — it caches a computed value. useCallback memoizes the FUNCTION ITSELF — it caches a function reference. useMemo(() => compute(), [deps]) returns the computed value. useCallback(() => doSomething(), [deps]) returns the function. Use useMemo for expensive calculations, useCallback for functions passed to memoized child components or used as effect dependencies."
Q: When should you NOT use React.memo?
"Don't use React.memo when: the component always receives new props on every parent render (memo can never help because props always differ), the component is cheap to render (the comparison overhead might exceed the rendering cost), or the component is rarely rendered anyway. React.memo is beneficial when a component is expensive to render and receives the same props frequently."
Q: When would you choose useReducer over useState?
"useReducer is better when state logic is complex — multiple related state values that update together, next state depends on previous state in non-trivial ways, or state transitions have specific named types. It centralizes all state transitions in one pure reducer function, making the logic easier to test and reason about. When you find yourself writing many useState hooks for related state, useReducer is usually the right choice."
Q: What problem does useContext solve?
"useContext solves props drilling — the problem of passing data through many intermediate components that don't use the data themselves, just to reach a deeply nested component that does. Context lets you broadcast a value to any component in the tree, regardless of depth, without threading it through every level as props."
Key Points to Remember
| Concept | Key Takeaway |
|---|---|
| Custom hooks | Function starting with "use". Extracts reusable stateful logic. Follows Rules of Hooks. |
| useMemo | Memoizes a computed VALUE. Recalculates only when dependencies change. |
| useCallback | Memoizes a FUNCTION reference. Prevents new function on every render. |
| React.memo | HOC that skips re-render if props are shallowly equal. Works with useCallback. |
| Optimize wisely | Measure first! Memoization has overhead. Don't add everywhere blindly. |
| useReducer | For complex state with multiple related values. Centralizes transitions in a reducer. |
| useContext | Share data without prop drilling. Consumers re-render when context value changes. |
What's Next?
In Chapter 10, we make our app beautiful with Tailwind CSS and learn the different styling options in React!
Keep coding, keep learning! See you in the next one!
Post a Comment