Himanshu SharmaSoftware engineer
Let’s talk

React · Lesson 29 of 38

useMemo, useCallback and memo

Learn why React components re-render and when memo, useMemo and useCallback help. Measure first: most components need none of them.

20 minSharing state and performanceUpdated 24 October 2026

By the end of this lessonExplain why a component re-renders, apply memo, useMemo and useCallback correctly, and judge when they are worth using.

React is fast by default, so most apps need no special tricks. But once in a while a screen feels sluggish: typing in a box lags, or a large list flickers. This lesson explains why components re-render, and the three tools React gives you to avoid unnecessary work. The most important advice comes first: measure before you optimise.

When does a component render?

A component renders again when:

  1. its own state changes (a setter is called), or
  2. its parent renders (all children render again by default), or
  3. a context it reads changes.

The second point surprises beginners: a child re-renders along with its parent even when its props are exactly the same.

jsx
import { useState } from "react";

function Child() {
  console.log("Child rendered");
  return <p>I am a child with no props.</p>;
}

function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Parent count: {count}</button>
      <Child />
    </div>
  );
}

export default App;
result
Parent count: 1
I am a child with no props.
CONSOLE
Child rendered
Child rendered
Child rendered
Child rendered

The log shows Child rendered twice at the start and twice more after one click (StrictMode renders everything twice in development), even though Child takes no props at all. Re-rendering means calling the function again to compute the JSX. React then compares it with the previous result, and updates the real page only where something differs, so the page itself is not rebuilt. For small components this is cheap and nobody notices. It matters when a component is expensive to render (a huge list, heavy calculations) and renders often.

memo: skip a component when props are equal

Wrap a component in memo and React remembers its last result. If the next render gives it props that are shallowly equal (each prop Object.is-equal to the previous), React skips it:

jsx
import { memo, useState } from "react";

const Child = memo(function Child({ name }) {
  console.log("Child rendered for", name);
  return <p>Hello, {name}</p>;
});

function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Parent count: {count}</button>
      <Child name="Asha" />
    </div>
  );
}

export default App;
result
Parent count: 1
Hello, Asha
CONSOLE
Child rendered for Asha
Child rendered for Asha

Now the click re-renders the parent but not the child: the only log lines are the two from the first render. memo compares name ("Asha" equals "Asha") and skips the work. Remember that a memoised component still re-renders if its own state or a context it uses changes; memo only guards against re-renders caused by the parent.

Why memo seems not to work: new objects and functions

memo compares by identity. Strings and numbers compare by value, but objects, arrays and functions created inside the parent are new on every render, so they never match:

jsx
import { memo, useState } from "react";

const Child = memo(function Child({ onSelect }) {
  console.log("Child rendered");
  return <button onClick={onSelect}>Select</button>;
});

function App() {
  const [count, setCount] = useState(0);

  function handleSelect() {        // a new function on every render of App
    console.log("selected");
  }

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Parent count: {count}</button>
      <Child onSelect={handleSelect} />
    </div>
  );
}

export default App;
result
Parent count: 1Select
CONSOLE
Child rendered
Child rendered
Child rendered
Child rendered

Even though the child is wrapped in memo, it rendered again after the click (four log lines in total), because handleSelect is a different function each time. This is the situation where useCallback helps.

useCallback: keep the same function

useCallback(fn, dependencies) returns the same function object between renders, until a dependency changes:

jsx
import { memo, useCallback, useState } from "react";

const Child = memo(function Child({ onSelect }) {
  console.log("Child rendered");
  return <button onClick={onSelect}>Select</button>;
});

function App() {
  const [count, setCount] = useState(0);

  const handleSelect = useCallback(() => {
    console.log("selected");
  }, []);                          // no reactive values used, so no dependencies

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Parent count: {count}</button>
      <Child onSelect={handleSelect} />
    </div>
  );
}

export default App;
result
Parent count: 1Select
CONSOLE
Child rendered
Child rendered

Now handleSelect keeps its identity, memo sees equal props, and the child is skipped after the click. useCallback is useful only together with memo on the receiving component (or when the function is a dependency of an effect or another hook). On its own, wrapping every function in useCallback just adds work. The dependency rules are the same as for effects: list every reactive value the function reads.

useMemo: cache a calculated value

useMemo(calculate, dependencies) runs calculate and stores the result. On later renders it returns the stored result unless a dependency changed. Use it for a slow calculation, or to give an object or array a stable identity:

jsx
import { useMemo, useState } from "react";

function slowSquareSum(n) {
  console.log("calculating for", n);
  let total = 0;
  for (let i = 1; i <= n * 1000000; i++) total += 1;      // pretend this is expensive
  return total;
}

function App() {
  const [n, setN] = useState(1);
  const [other, setOther] = useState(0);

  const result = useMemo(() => slowSquareSum(n), [n]);

  return (
    <div>
      <input value={n} onChange={e => setN(Number(e.target.value) || 0)} />
      <button className="other" onClick={() => setOther(other + 1)}>Unrelated state: {other}</button>
      <p>Result: {result}</p>
    </div>
  );
}

export default App;
result
Unrelated state: 2
Result: 13000000
CONSOLE
calculating for 1
calculating for 1
calculating for 13
calculating for 13

The log shows the calculation ran for the initial value 1 and for 13 (the test typed a 3 after the existing 1), each twice because of StrictMode. Pressing the unrelated button twice did not run it again, because n did not change. Without useMemo, each click would repeat the loop. Notice also that the result is a value, while useCallback caches a function; in fact useCallback(fn, deps) is the same as useMemo(() => fn, deps).

A second use is making an object stable for a child or an effect dependency:

jsx
const options = useMemo(() => ({ theme, size }), [theme, size]);   // same object until theme or size changes

Measure first

Memoization is not free. Each memo, useMemo and useCallback costs memory and a comparison on every render, and makes code harder to read and easier to get wrong (a missing dependency gives stale values). So do not sprinkle them everywhere. Instead:

  1. Notice a real problem: typing lags, scrolling stutters, a screen takes a long time.
  2. Measure with the Profiler tab of the React Developer Tools browser extension. Record an interaction and look at which components render and how long each takes.
  3. Fix the cause with the cheapest tool, in this order:
    • Move state down so that less of the tree re-renders when it changes (the component that owns a fast-changing input should not also own a big list).
    • Pass slow children as children (they are created by a parent above the changing state, so they do not re-render).
    • Reduce the work: paginate or virtualise long lists, avoid heavy calculations during render.
    • Only then reach for memo, useMemo and useCallback.
  4. Measure again to confirm the improvement.

Here is the "move state down" idea. The slow list renders on every keystroke because the input's state lives in the same component:

jsx
// Before: typing re-renders SlowList
function App() {
  const [text, setText] = useState("");
  return (<><input value={text} onChange={e => setText(e.target.value)} /><SlowList /></>);
}

// After: typing only re-renders SearchBox
function SearchBox() {
  const [text, setText] = useState("");
  return <input value={text} onChange={e => setText(e.target.value)} />;
}
function App() {
  return (<><SearchBox /><SlowList /></>);
}

The React Compiler

React has a build-time tool called the React Compiler that adds memoization automatically, by analysing your components and applying memo-like caching where it is safe. With it, hand-written useMemo, useCallback and memo are needed much less often, mostly for special cases. It requires components that follow the rules (pure rendering, no mutation of state), which is a good reason to keep to the lessons so far. The Vite template does not enable it by default; see the React documentation to turn it on. Whether or not you use it, the ideas in this lesson explain what it is doing.

Common mistakes

  • Wrapping everything in memo, useMemo and useCallback "just in case".
  • Using useCallback for a function passed to a component that is not memoised, which has no effect.
  • Forgetting a dependency, so a memoised value or function uses stale data.
  • Passing a new object, array or inline function to a memoised child and wondering why memo does nothing.
  • Memoising something cheap: the comparison costs about as much as the work.
  • Using useMemo for correctness (it is a performance hint; React may discard cached values). Your code must work without it.
  • Mutating a memoised object, which hides changes from React.

Exercise

  1. Add console.log to a parent and two children, then find out which children re-render when the parent's state changes. Wrap one in memo and compare.
  2. Make a list of 2,000 items with a search box. Time typing with and without moving the search state into its own component.
  3. In the useMemo example, replace the loop with [...Array(n * 1000)].map(...) and see how large you can make n before the page gets sluggish. Does useMemo still help when n changes?
  4. Open the React Developer Tools Profiler on any page of your own project and record one interaction. Which component took longest?

Key points

  • By default a component re-renders whenever its parent does, even if its props are the same; this is usually fast enough.
  • memo(Component) skips re-rendering when props are shallowly equal to the previous props.
  • useMemo caches the result of a calculation and useCallback caches a function between renders, so that they stay the same object until their dependencies change.
  • Measure before optimising: most apps need very little memoization, and the React Compiler can add it automatically.

Check yourself

1. A parent re-renders because of its state. What happens to a child that gets the same props?
2. Why can a memoised child still re-render if the parent passes an inline function as a prop?
3. What does useMemo(() => compute(a), [a]) return?
4. What is the first step before adding useMemo or memo?