import { memo } from "react";
const Child = memo(function Child({ name }) {
console.log("Child rendered");
return <p>Hello, {name}</p>;
});Wrapping a component in memo() tells React to skip re-rendering it if its props haven't actually changed since the last render โ directly solving the 'Child re-rendered even though it doesn't use count' scenario from Chapter 30.
import { useMemo, useCallback } from "react";
function ProductList({ products, searchTerm }) {
// useMemo - only recalculates when products or searchTerm actually change
const filtered = useMemo(() => {
return products.filter((p) => p.name.includes(searchTerm));
}, [products, searchTerm]);
// useCallback - returns the SAME function reference across renders, unless its dependencies change
const handleSelect = useCallback((id) => {
console.log("Selected:", id);
}, []);
return <ProductGrid items={filtered} onSelect={handleSelect} />;
}useMemo caches an expensive calculated value; useCallback caches a function reference. Both matter most when passed down to a memo()-wrapped child โ without them, a parent re-render creates a brand-new array or function every time, defeating memo()'s comparison entirely.
import { lazy, Suspense } from "react";
const Dashboard = lazy(() => import("./Dashboard.jsx"));
function App() {
return (
<Suspense fallback={<p>Loading dashboard...</p>}>
<Dashboard />
</Suspense>
);
}lazy() splits a component into its own separate JavaScript file, only downloaded when it's actually needed โ for example, a Dashboard page only loaded after login, rather than bundled into the initial page load everyone downloads. <Suspense> shows a fallback UI while that chunk loads.
For lists with hundreds or thousands of items, rendering every single one at once โ even with memo() โ becomes slow. List virtualization (via libraries like react-window) renders only the items currently visible in the viewport, recycling DOM nodes as the user scrolls. And revisiting Chapter 22's search problem: the useDebounce custom hook built in Chapter 29 is itself a performance optimization โ reducing how often an expensive operation (an API call, a large filter) runs.
These tools have real overhead of their own (extra comparisons on every render) and add code complexity. Wrapping every single component in memo() "just in case" often makes an app harder to reason about without any measurable benefit. The professional approach: build features normally first, then use React DevTools' Profiler to find actual, measured slowdowns before reaching for these optimizations.
Wrap a child component in memo() and add a console.log inside it, then verify in the console that it no longer re-renders when an unrelated piece of parent state changes.
import { useState, memo } from "react";
const ExpensiveChild = memo(function ExpensiveChild() {
console.log("ExpensiveChild rendered");
return <p>I only re-render when my own props change.</p>;
});
function App() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<ExpensiveChild />
</>
);
}
Q Should I use memo() on every component by default?
No โ for most simple components, the cost of re-rendering is negligible, and adding memo() everywhere adds complexity without measurable benefit. Reserve it for components that are genuinely expensive to render or re-render very frequently with unchanged props.
Q What's the difference between lazy loading and useMemo?
They solve completely different problems: lazy loading defers downloading a component's code until it's needed (reducing initial bundle size), while useMemo caches the result of a calculation within an already-loaded component (reducing repeated computation).