Skip to content

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.

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.

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-renders

This 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.

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:

  • spentByCategory wrapped in useMemo (same object reference when transactions are unchanged)
  • onUpdate, onDelete, onAddTransaction wrapped in useCallback (same function references)

All three work together. React.memo alone does nothing if the props are new references every render.

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.

  1. Wrap CategoryCard with memo. Add a console.log at the top of the component.
  2. Add an income source and observe which cards re-render (without memoization: all 8, with: none, if callbacks are stable).
  3. Add useCallback to addTransaction and deleteTransaction in App.jsx.
  4. Add useMemo to spentByCategory.
  5. Verify that adding income no longer causes any CategoryCard to re-render.
  • React.memo skips re-rendering a component if its props have not changed (by reference).
  • Props that are new objects or functions every render defeat memoization — use useMemo and useCallback to stabilize them.
  • React.memo + useCallback + useMemo is the full memoization triad — they only work together.
  • Do not add React.memo everywhere by default — measure first, then apply where it helps.