A Look Behind the Scenes of React & Optimization TechniquesEntry 3 of 10

memo(): Skipping Re-Renders When Props Are Unchanged

memo(Component) returns a version of the component that skips re-executing when its parent re-renders but its props haven't changed.

react.devmemo

Notes

  • Import it from react (import { memo } from 'react'). memo() and React.memo() are the same function; it's a wrapper around a component, not a hook, and it isn't new in React 19.
  • Every time the parent re-renders, memo() compares the old and new props, one by one, with Object.is:
    • Equal → the component function does not execute.
    • Not equal → it executes as normal.
  • memo() only blocks re-renders caused by the parent. The component still re-renders when its own state or a context it consumes changes.
  • Objects, arrays, and functions created during the parent's render are new every time, so they always count as "changed." Pair memo() with useMemo/useCallback for such props, or let the React Compiler handle it (Entry 9).

Don't overuse it:

  • Use it as high up in the tree as possible: blocking one component also blocks all its children.
  • Checking props costs performance. Don't wrap every component.
  • Don't use it on components whose props change frequently; the check would almost always fail and just add work.

Correction: Always define components at module level, never inside another component. A component defined (or wrapped in memo()) inside another component's body is a new component type on every render, so React unmounts and remounts it each time, losing its state. memo() can't help in that case.

What / Why / When

What

A props-equality check in front of a component.

Why

Skips expensive re-renders when a parent re-renders for unrelated reasons.

When

A component is expensive to render, re-renders often, and usually receives the same props.

Use case

A Counter that should not re-run while the user types in an unrelated input in App.

Code compare

React
import { memo, useState } from "react";​type CounterProps = { initialCount: number };​export const Counter = memo(function Counter({ initialCount }: CounterProps) {  const initialCountIsPrime = isPrime(initialCount);  const [counter, setCounter] = useState(initialCount);​  function handleDecrement() {    setCounter((prev) => prev - 1);  }​  function handleIncrement() {    setCounter((prev) => prev + 1);  }​  return (    <section>      <p>The initial count is {initialCountIsPrime ? "" : "not "}a prime number.</p>      <button onClick={handleDecrement}>-</button>      <span>{counter}</span>      <button onClick={handleIncrement}>+</button>    </section>  );});
React
// ❌ Defined inside App: a new component type every renderfunction App({ players }: { players: PlayerData[] }) {  const PlayerItem = memo(function PlayerItem({ name }: { name: string }) {    return <li>{name}</li>;  });  return (    <ul>      {players.map((p) => (        <PlayerItem key={p.id} name={p.name} />      ))}    </ul>  );}​// ✅ Defined at module leveltype PlayerData = { id: string; name: string };​const PlayerItem = memo(function PlayerItem({ name }: { name: string }) {  return <li>{name}</li>;});​export function App({ players }: { players: PlayerData[] }) {  return (    <ul>      {players.map((p) => (        <PlayerItem key={p.id} name={p.name} />      ))}    </ul>  );}

The closest equivalent is ChangeDetectionStrategy.OnPush: Angular skips checking the component unless an input reference changes, an event fires inside it, or a signal it reads changes.

Angular
import { ChangeDetectionStrategy, Component, computed, input, linkedSignal } from "@angular/core";​@Component({  selector: "app-counter",  changeDetection: ChangeDetectionStrategy.OnPush,  template: `    <section>      <p>The initial count is {{ initialCountIsPrime() ? "" : "not " }}a prime number.</p>      <button (click)="counter.update((c) => c - 1)">-</button>      <span>{{ counter() }}</span>      <button (click)="counter.update((c) => c + 1)">+</button>    </section>  `,})export class CounterComponent {  initialCount = input.required<number>();  initialCountIsPrime = computed(() => isPrime(this.initialCount()));  counter = linkedSignal(() => this.initialCount()); // resets when the input changes}

Differences and gotchas

  • Is memo() like OnPush? Yes, conceptually: both skip work when inputs are unchanged by reference. OnPush is a per-component setting, and components built entirely on signals already get most of its benefit.
  • Your notes described memo() as a React 19 hook for inline components. That's incorrect: it's a long-standing wrapper function, and it must not be used inside another component.
  • memo(Component, arePropsEqual) accepts a custom comparison function. Use it rarely; a wrong comparison causes stale UI.

Quick reference

ReactAngular
memo(Component)changeDetection: ChangeDetectionStrategy.OnPush
Shallow props comparison (Object.is)Input reference comparison
Still re-renders on own state/contextStill checks on own events/signals