React.memo
By default, when a parent re-renders, all its children re-render too — even if their props did not change. React.memo wraps a component and tells React to skip re-rendering it if its props are the same as last time.
The basic usage
Section titled “The basic usage”import { memo } from 'react';
const CategoryCard = memo(function CategoryCard({ category, spentByCategory, onUpdate, onDelete, onAddTransaction }) { console.log('CategoryCard rendered:', category.name); return ( <div className="category-card card"> <h3>{category.name}</h3> {/* ... */} </div> );});
export default CategoryCard;Or wrap the exported component:
function CategoryCard({ category, ... }) { ... }export default memo(CategoryCard);Now React compares each prop by reference before re-rendering. If all props are the same references as last time, React skips the render entirely and reuses the previous output.
Reference equality
Section titled “Reference equality”React.memo uses Object.is (strict reference equality) to compare props. This matters:
// Primitive props — compared by value<CategoryCard budgeted={1200} name="Housing" />// 1200 === 1200 ✓, 'Housing' === 'Housing' ✓ — skipped if unchanged
// Object/array props — compared by reference<CategoryCard category={{ id: 'housing', name: 'Housing' }} />// {} !== {} — new object every render → always re-rendersThis is why useCallback and useMemo matter in combination with React.memo. If you pass a function or object that is recreated every render, React.memo cannot help — the prop always looks changed.
ZeroBudget: why CategoryCard benefits
Section titled “ZeroBudget: why CategoryCard benefits”App re-renders when any state changes — adding income, navigating months, typing in a form. Without React.memo, all 8 CategoryCard components re-render every time. With React.memo, a card only re-renders if its category object, the spentByCategory map, or one of its callback functions changed.
For React.memo to work here, you also need:
spentByCategorywrapped inuseMemo(same object reference when transactions are unchanged)onUpdate,onDelete,onAddTransactionwrapped inuseCallback(same function references)
All three work together. React.memo alone does nothing if the props are new references every render.
Custom comparison
Section titled “Custom comparison”For complex cases, pass a comparison function as the second argument:
const CategoryCard = memo(CategoryCardComponent, (prevProps, nextProps) => { // return true to skip re-render (props are "equal") // return false to re-render return prevProps.category === nextProps.category && prevProps.spentByCategory === nextProps.spentByCategory;});Use this sparingly. Getting the comparison wrong (accidentally returning true when props changed) causes silent staleness bugs that are very hard to find.
Exercise
Section titled “Exercise”- Wrap
CategoryCardwithmemo. Add aconsole.logat the top of the component. - Add an income source and observe which cards re-render (without memoization: all 8, with: none, if callbacks are stable).
- Add
useCallbacktoaddTransactionanddeleteTransactioninApp.jsx. - Add
useMemotospentByCategory. - Verify that adding income no longer causes any
CategoryCardto re-render.
React.memoskips re-rendering a component if its props have not changed (by reference).- Props that are new objects or functions every render defeat memoization — use
useMemoanduseCallbackto stabilize them. React.memo+useCallback+useMemois the full memoization triad — they only work together.- Do not add
React.memoeverywhere by default — measure first, then apply where it helps.