跪拜 Guibai
← Back to the summary

React's Render Cascade and the One Hook That Stops It

image.png

image.png

One-Sentence Conclusion

useCallback caches function references and, when paired with React.memo, prevents unnecessary re-renders in child components, solving the performance problem of "parent updates causing cascading child re-renders."


1. Root of the Problem: Why Do We Need useCallback?

1.1 The Observable Phenomenon

Look at the code in App.jsx:

function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Shaolin Team');

  return (
    <>
      <button onClick={() => setCount(count + 1)}>Click Count {count}</button>
      <button onClick={() => setName('Emei Team')}>Click Name</button>
      <RegularChild name={name} />
      <MemoChild name={name} />
    </>
  );
}

Phenomenon: When clicking the "Click Count" button, count changes but name does not—yet RegularChild still re-renders.

1.2 Root Cause Analysis

React's rendering mechanism dictates that when a parent component re-renders, all its child components re-render by default. This is not a bug but a conservative strategy—React doesn't know whether a child component depends on some changed state in the parent.

The logic chain here is:

count changes
  → App function re-executes
    → All JSX is regenerated
      → RegularChild re-renders (even though name hasn't changed)

1.3 memo Enters the Scene—But It's Not Enough

React.memo skips rendering for child components whose props haven't changed by performing a shallow comparison:

const MemoChild = memo(({ name }) => {
  console.log('MemoChild component renders');
  return <h1>{name}</h1>;
});

For primitive value props (like the string name), memo works perfectly—if name hasn't changed, it doesn't render.

But when props contain functions, the problem reappears:

// If an inline function is passed to MemoChild
<MemoChild name={name} onChange={() => setName('New Team Name')} />

Every time App renders, () => setName('New Team Name') creates a brand new function reference. When memo performs a shallow comparison, it finds that onChange has changed → the child component still re-renders.

This is where useCallback comes into play.


2. What is useCallback? — Core Definition

2.1 In a Nutshell

useCallback is a Hook that caches a function reference, only recreating the function when its dependencies change.

2.2 Basic Syntax

const memoizedFn = useCallback(fn, [deps]);
Element Meaning
fn The function to cache
deps Dependency array; fn is only recreated when deps change
Return Value The cached function reference (reference stays the same if deps haven't changed)

2.3 Core Mechanism Diagram

Initial Render:  Create fn_v1 → useCallback caches → returns fn_v1 reference
          ↓
Update Render (deps unchanged):  Skip creation → directly return fn_v1 reference (same reference!)
          ↓
Update Render (deps changed):  Create fn_v2 → update cache → return fn_v2 reference

Contrast with the standard approach:

Standard: Creates a new function on every render → reference differs each time → memo fails
useCallback: Returns the same reference when deps are unchanged → memo works → child component skips rendering

3. Complete Workflow: The memo + useCallback Collaboration

3.1 Problem → Solution Mapping

Problem Chain:
  Parent renders → Function reference changes → memo shallow comparison fails → Child component renders unnecessarily

Solution:
  Parent renders → useCallback preserves function reference → memo shallow comparison passes → Child component skips rendering

3.2 Refactoring Example

An evolved version based on App.jsx:

import { useState, memo, useCallback } from 'react';

function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Shaolin Team');

  // ✅ Cache the callback with useCallback; reference stays the same as long as [name] doesn't change
  const handleChangeName = useCallback(() => {
    setName('Emei Team');
  }, []); // Empty dependency array: this function never changes

  // ❌ Without useCallback: a new function is created on every render
  // const handleChangeName = () => setName('Emei Team');

  console.log('APP component renders');
  return (
    <>
      <button onClick={() => setCount(count + 1)}>Click Count {count}</button>
      <button onClick={handleChangeName}>Click Name</button>
      <RegularChild name={name} />
      <MemoChild name={name} onAction={handleChangeName} />
    </>
  );
}

const MemoChild = memo(({ name, onAction }) => {
  console.log('MemoChild component renders');
  return <h1>{name}</h1>;
});

Effect: When clicking "Click Count", the reference for handleChangeName remains unchanged → MemoChild's props overall remain unchanged → memo skips the render.


4. Pyramid Summary: From Phenomenon to Essence

                    ┌──────────────────────────────────┐
                    │  useCallback solves the problem  │
                    │  of function reference drift,    │
                    │  allowing memo to truly work      │
                    └──────────────────────────────────┘
                                    ▲
              ┌─────────────────────┴─────────────────────┐
              │                                           │
    ┌─────────────────┐                         ┌─────────────────┐
    │  memo is not enough│                         │  useCallback     │
    │  Function props   │                         │  caches function │
    │  cause shallow    │                         │  references;     │
    │  comparison to    │                         │  ref unchanged   │
    │  always fail      │                         │  if deps unchanged│
    └─────────────────┘                         └─────────────────┘
              ▲                                           ▲
              │                                           │
    ┌─────────────────┐                         ┌─────────────────┐
    │  React Rendering │                         │  Closures & Dep  │
    │  Mechanism:      │                         │  Tracking: uses  │
    │  Parent renders →│                         │  deps array to   │
    │  children render │                         │  decide when to  │
    │  by default      │                         │  refresh cache   │
    └─────────────────┘                         └─────────────────┘

Key Cognitive Leaps

  1. Accept Reality: React defaults to "better to over-render than under-render."
  2. First Line of Defense: memo uses shallow comparison to block renders when props haven't changed.
  3. Defense Gap: Function references are inherently unstable; memo cannot guard against them.
  4. Ultimate Patch: useCallback stabilizes function references, sealing the defense line.

Usage Guidelines


5. Extension: The Relationship Between useMemo and useCallback

Hook What it caches Essential Equivalence
useCallback Function reference useMemo(() => fn, deps)
useMemo Computed result useCallback(() => value, deps)()

They are two sides of the same coin: useCallback is syntactic sugar for useMemo, both used to stabilize references and cooperate with memo for precise render control.


Core Idea: The essence of React performance optimization is not "making renders faster," but "preventing renders that shouldn't happen." memo is responsible for judging "should it render?", and useCallback is responsible for ensuring memo's judgment criteria aren't corrupted. The two are a symbiotic pair.