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.
Notes
- Import it from
react(import { memo } from 'react').memo()andReact.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, withObject.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()withuseMemo/useCallbackfor 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
Counterthat should not re-run while the user types in an unrelated input inApp.
Code compare
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> );});// ❌ 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.
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()likeOnPush? Yes, conceptually: both skip work when inputs are unchanged by reference.OnPushis 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
| React | Angular |
|---|---|
memo(Component) | changeDetection: ChangeDetectionStrategy.OnPush |
Shallow props comparison (Object.is) | Input reference comparison |
| Still re-renders on own state/context | Still checks on own events/signals |