AiTechWorlds
AiTechWorlds
React.memo is a wrapper that skips a component's re render when its props haven't actually changed.
Think of a mail carrier who re delivers every letter on the street each time one household gets new mail. That's a component tree without memo one state change re renders everything downstream, needed or not. React.memo is the carrier learning to check each mailbox and skip the ones with nothing new. Most "slow" React apps are actually rendering correctly they just do more of this unnecessary redelivery than they need to.
A component re renders when:
Notice what's NOT on the list: the props themselves changing. By default, if a parent re renders, ALL its children re render, even if their props are identical.
function Counter() {
const [count, setCount] = useState(0);
console.log("Counter rendered");
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<StaticContent /> {/* This re-renders on every count change! */}
</div>
);
}
function StaticContent() {
console.log("StaticContent rendered"); // This logs on every click
return <div>I never change</div>;
}Every click on the counter re renders StaticContent, even though it has no props and never changes.
Wrap a component in React.memo to make it only re render when its props change:
const StaticContent = React.memo(function StaticContent() {
console.log("StaticContent rendered"); // Now only logs on mount
return <div>I never change</div>;
});React does a shallow comparison of all props. If every prop passes === equality, the component skips the render.
Shallow comparison means reference equality for objects and functions:
function Parent() {
const [count, setCount] = useState(0);
// Wrong: New object on every render — memo won't help
const config = { theme: "dark", language: "en" };
// Wrong: New function on every render — memo won't help
const handleClick = () => console.log("clicked");
return <MemoizedChild config={config} onClick={handleClick} />;
}To make React.memo effective, you need stable prop references:
function Parent() {
const [count, setCount] = useState(0);
// Correct: Stable reference — only changes if theme/language change
const config = useMemo(() => ({ theme: "dark", language: "en" }), []);
// Correct: Stable function reference
const handleClick = useCallback(() => console.log("clicked"), []);
return <MemoizedChild config={config} onClick={handleClick} />;
}
const MemoizedChild = React.memo(function MemoizedChild({ config, onClick }) {
return <button onClick={onClick}>Theme: {config.theme}</button>;
});setState functions from useState are always stable they never change identity between renders. You don't need useCallback to wrap functions that only call a setter:
function Parent() {
const [items, setItems] = useState<string[]>([]);
// Correct: setItems is stable — useCallback not needed here
function addItem(item: string) {
setItems(prev => [...prev, item]);
}
// But the function itself is recreated on every render
// If you pass it to a memo'd component, you need useCallback:
const stableAddItem = useCallback((item: string) => {
setItems(prev => [...prev, item]);
}, []); // setItems is stable, so empty deps is correct
return <MemoizedInput onAdd={stableAddItem} />;
}Before reaching for React.memo, consider moving state closer to where it's used. When state lives at a higher level than necessary, it causes more components to re render.
// Wrong: Modal state in App causes everything to re-render on toggle
function App() {
const [modalOpen, setModalOpen] = useState(false);
return (
<div>
<ExpensiveList /> {/* Re-renders when modal opens/closes */}
<ExpensiveChart /> {/* Re-renders when modal opens/closes */}
<Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} />
<button onClick={() => setModalOpen(true)}>Open</button>
</div>
);
}
// Correct: Extract the modal + trigger into its own component
function App() {
return (
<div>
<ExpensiveList /> {/* No longer affected by modal state */}
<ExpensiveChart /> {/* No longer affected by modal state */}
<DeleteConfirmation /> {/* Modal state lives here */}
</div>
);
}
function DeleteConfirmation() {
const [open, setOpen] = useState(false); // State scoped to this component
return (
<>
<button onClick={() => setOpen(true)}>Delete</button>
<Modal isOpen={open} onClose={() => setOpen(false)} />
</>
);
}No memo, no useCallback just better structure. This is usually the right fix first.
Another structural optimization: pass components as children instead of rendering them directly. Children of a component only re render if their own props change the parent's state changes don't affect them:
// Wrong: ExpensiveChild re-renders when Parent state changes
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<ExpensiveChild /> {/* Re-renders on count change */}
</div>
);
}
// Correct: Children passed from above don't re-render with Parent
function Parent({ children }: { children: React.ReactNode }) {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
{children} {/* Not affected by count state */}
</div>
);
}
// App creates the child, Parent just slots it in
function App() {
return (
<Parent>
<ExpensiveChild /> {/* Only re-renders if App re-renders */}
</Parent>
);
}For large components not needed on initial load, use React.lazy:
import { lazy, Suspense } from "react";
// The import is deferred until this component is first rendered
const RichEditor = lazy(() => import("./RichEditor"));
const VideoPlayer = lazy(() => import("./VideoPlayer"));
function CourseEditor() {
const [activeTab, setActiveTab] = useState("content");
return (
<div>
<TabBar active={activeTab} onChange={setActiveTab} />
<Suspense fallback={<div>Loading editor...</div>}>
{activeTab === "content" && <RichEditor />}
{activeTab === "preview" && <VideoPlayer />}
</Suspense>
</div>
);
}RichEditor and VideoPlayer are only downloaded when the user clicks that tab. The initial bundle is smaller. Page loads faster.
Before reaching for memoization:
useMemo + useCallback if specific components are measured to be slowPremature optimization makes code harder to read. Measure, then optimize the things that actually matter.
Next lesson: Next.js 15 architecture overview the full picture of how Next.js works.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises