跪拜 Guibai
← Back to the summary

React Context, Custom Hooks, and the Rendering Traps Between Them

Today's Highlights

Knowledge Relationships

Component communication is the background problem → useContext is the cross-level communication solution → Provider's value change drives data subscribers to render → Encapsulating custom Hooks modularizes Context consumption logic → useMouse demonstrates how to abstract event listening + state management into a reusable Hook → High-frequency events introduce the rendering difference between useState and useRef → useEffect cleanup function ensures no leaks.


Four Relationships of Component Communication

Knowledge Points

A React page is composed of components, and components need to pass data to each other—this is component communication. Based on the positional relationship of components in the tree, there are four communication methods.

Key Code

Parent-Child: props (parent→child) + callback functions (child→parent)
Siblings: Lift state up to a common parent component
Grandparent-Grandchild: useContext tunnel direct access
Strangers: State management libraries (Redux, Zustand)

Execution Process

Parent-Child is the most basic model: the parent writes data onto the child component's tag attributes, and the child receives it via function parameters. The child cannot directly modify the parent's data; it can only "notify" the parent through callback functions passed down by the parent.

Siblings have no direct channel; shared data must be lifted to the nearest common parent component, which manages it uniformly and distributes it to both children. Essentially, it's two parent-child communications pieced together.

Grandparent-Grandchild is the pain point of prop drilling: the grandparent has data, the grandchild needs it, and the intermediate parent is forced to receive and forward props it doesn't care about. The deeper the hierarchy, the harder it is to maintain.

Strangers are two components with no common ancestor in the tree, typically appearing in large applications, requiring a dedicated state management library.

Why It's Designed This Way

React insists on unidirectional data flow: data flows from parent to child, never in reverse. The benefit is that data changes are traceable—only the component holding the state can modify it; other components can only "apply" via callbacks. If data could be modified arbitrarily in both directions, you'd never find the culprit when debugging bugs.

Usage Boundaries

Relationship Solution Applicable Scenario
Parent-Child props Shallow hierarchy, one-to-one
Siblings State lifting Few siblings, close common parent
Grandparent-Grandchild useContext Intermediate layers don't need to perceive the data
Strangers State library Global sharing, complex relationships

Self-Test

  1. Why must sibling component communication go through the parent component?
  2. What is the essence of state lifting?

Reference Answers

  1. React data can only flow from top to bottom; there is no horizontal channel between two siblings, so data must be lifted to a common parent component and then distributed.
  2. The essence is two parent-child communications pieced together: Sibling A notifies the parent via callback, and the parent passes it to Sibling B via props.

useContext Three-Step Template

Knowledge Points

useContext is a cross-level data passing solution provided by React. It opens a "data tunnel" in the component tree, allowing descendant components to directly fetch data from ancestors without passing props layer by layer.

Real Problem

In the following scenario, Father doesn't need theme but must receive and forward it:

function App() {
  const theme = 'dark'
  return <Father theme={theme} />
}

function Father({ theme }) {          // Doesn't use it but must write it
  return <Child theme={theme} />      // Must pass it down
}

function Child({ theme }) {
  return <div>{theme}</div>           // Actually uses it
}

When the hierarchy deepens, all intermediate components get polluted. useContext solves this problem.

Key Code

Step 1: Create Context

import { createContext } from 'react'

export const ThemeContext = createContext('light')

The parameter of createContext is the default value, not the shared data. The default value only serves as a fallback when a component is not wrapped by a Provider. The actual shared data is determined by the Provider's value.

Step 2: Provider Broadcast

function App() {
  const [theme, setTheme] = useState('light')

  return (
    <ThemeContext.Provider value={theme}>
      <Page />
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </ThemeContext.Provider>
  )
}

Provider is placed at any position in the component tree; its scope is all descendant components it wraps. Provider is not global—its scope extends only as far as it is placed.

Step 3: useContext Consumption

import { useContext } from 'react'
import { ThemeContext } from '../ThemeContext'

function Child() {
  const theme = useContext(ThemeContext)
  return <div>{theme}</div>
}

Execution Process

Click "Toggle Theme" button
  → setTheme('dark')  // App's state changes
    → App re-executes, theme variable becomes 'dark'
      → <Provider value="dark">  // Broadcasts new value
        → All components calling useContext(ThemeContext) are marked dirty
          → Page re-renders, gets 'dark'
          → Child re-renders, gets 'dark'

Why It's Called "Consumption"

React uses the "Producer-Consumer" pattern for naming: Provider is the producer (provides data), useContext is the consumer (subscribes to data). "Consumption" is not "fetch once and done"—it's a continuous subscription: when Provider's value changes, consumers automatically get the new value and re-render.

Analogy: The water plant is the Provider, the pipe is Context, your faucet is the Consumer. When the plant's pressure changes, your water immediately follows. You are "consuming" tap water, not going to the plant to "fetch" water.

Easy to Confuse

The parameter in createContext('default value') is not the shared data. The shared data is the Provider's value:

const ThemeContext = createContext('light')   // Default value

<ThemeContext.Provider value="dark">          // This is the shared data
  <Child />                                   // useContext → "dark"
</ThemeContext.Provider>

<Child />                                     // No Provider → "light" (default value fallback)

Usage Boundaries

Suitable for Context Not Suitable for Context
Global theme Frequently changing values (input fields)
Current user info Data used in only one or two components
Language settings Scenarios easily solved with props

Overusing Context makes component data sources obscure, and when value changes, all consumers re-render—not suitable for high-frequency update data.

Self-Test

  1. What is the parameter of createContext? What is its relationship with Provider's value?
  2. Is Provider global? Why?

Reference Answers

  1. The parameter of createContext is the default value, used as a fallback only when descendant components cannot find a Provider. Provider's value is the actual shared data, overriding the default value.
  2. No, it's not global. Provider's scope is the portion of the component tree it wraps, based on where it is placed. Components outside cannot access the data.

The Meaning of Encapsulating useTheme

Knowledge Points

Using useContext(ThemeContext) directly without encapsulation works too. Encapsulating it into useTheme() has three benefits.

Key Code

import { ThemeContext } from '../ThemeContext'
import { useContext } from 'react'

export function useTheme() {
  return useContext(ThemeContext)
}

Why It's Designed This Way

One: Fewer imports. Each consuming component doesn't need to import both useContext and ThemeContext; it only needs to import useTheme.

Two: Error validation can be added. If someone forgets to write the Provider, without encapsulation it silently returns the default value, making bugs hard to trace. With encapsulation:

export function useTheme() {
  const context = useContext(ThemeContext)
  if (context === null) {
    throw new Error('useTheme must be used inside a Provider!')
  }
  return context
}

Forgot to wrap Provider → immediate error, problem is obvious.

Three: Change implementation without disturbing consumers. Suppose later you don't depend on Context and switch to another solution; just modify the useTheme function alone, and all consuming components don't need a single line changed.

Usage Boundaries

Small projects might not feel the difference, but when components multiply and cross teams, the value of encapsulation becomes apparent. The hooks directory is part of project architecture.

Self-Test

  1. What is the greatest value of encapsulating useTheme?
  2. What is the hidden danger of using useContext(ThemeContext) directly without encapsulation?

Reference Answers

  1. It allows validation before retrieving the value, immediately throwing an error when Provider is forgotten instead of failing silently.
  2. Without validation, if a component is not wrapped by Provider, it silently gets the default value, making bugs hard to discover.

Context and Unidirectional Data Flow

Knowledge Points

Context does not change React's unidirectional data flow rules. Data still flows from top to bottom; it just takes a different path.

Execution Process

props method:  App → Page → Child    Takes the stairs, passes through every layer
Context method: App ═══════ Child     Takes the elevator, direct access

The direction is always ancestor to descendant, unidirectional downward. Child components still cannot reversely modify Provider's data, unless the Provider also passes setState down through value.

Easy to Confuse

props Context
Data flow direction Top→Bottom Top→Bottom
Intermediate components Must receive and forward Completely unaware
Changing data Parent's setState Parent's setState (passed via value)

Unidirectional data flow is React's iron law; Context doesn't break it, it just skips the code passing in intermediate layers.

Self-Test

  1. Does Context change React's unidirectional data flow?
  2. Can a child component reversely modify parent component data through Context?

Reference Answers

  1. No. Data always flows from ancestor to descendant; the flow direction hasn't changed.
  2. No. Unless the parent component also puts setState into value and passes it down—even then, the "modification" action happens in the child, but state ownership still belongs to the parent.

Who Triggers Who Renders

Knowledge Points

When Provider's value changes, components that subscribed to that Context using useContext will re-render. But even without subscribing, child components will also render because their parent component renders.

Key Code

// App.jsx
function App() {
  const [theme, setTheme] = useState('light')
  return (
    <ThemeContext.Provider value={theme}>
      <Page />
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle</button>
    </ThemeContext.Provider>
  )
}

// Page.jsx — uses useTheme
const Page = () => {
  const theme = useTheme()
  return <><Child /></>
}

// Child.jsx — also uses useTheme
function Child() {
  const theme = useTheme()
  return <button>{theme}</button>
}

Execution Process

Click toggle button
  → setTheme changes App's state
    → App re-renders
      → Page re-renders (two reasons: parent render + useContext subscription)
        → Child re-renders (two reasons: parent Page render + useContext subscription)

Page and Child both have two rendering reasons: parent component drags them along + useContext subscription. Using React.memo can block the parent render chain:

const Page = React.memo(() => {
  const theme = useTheme()
  return <><Child /></>
})

memo checks that Page's props haven't changed → blocks parent render chain. But useTheme() subscribes to Context → Provider value changed → memo cannot block it, Page still renders.

Why It's Designed This Way

Context's subscription mechanism is independent of the component tree's parent-child render chain. memo can only intercept renders driven by props changes, not renders driven by Context subscriptions. This is a design guarantee: when a component declares a dependency on a certain Context, no matter what wraps it in between, it will definitely update when the data changes.

Common Pitfalls

You can't look at just one point for why a child component renders. React.memo can block the parent render chain, but not Context subscriptions. Conversely, if a component doesn't subscribe to Context but has memo added, it won't render when Provider value changes.

Self-Test

  1. Page uses useTheme; when App re-renders, will Page definitely re-render? Can React.memo block it?
  2. If Page doesn't use useTheme but has React.memo added, and Provider value changes, will Page render?

Reference Answers

  1. Yes. memo cannot block renders triggered by Context subscriptions; Page still renders.
  2. No. Page doesn't subscribe to Context; memo finds props unchanged and won't render.

Custom Hook: useMouse

Knowledge Points

A custom Hook extracts reactive logic (useState, useEffect, etc.) from a component into a reusable function starting with use.

Real Problem

After writing mouse tracking functionality in App, if another page also needs it, you can't copy-paste. Abstract it into a custom Hook.

Key Code

import { useState, useEffect } from 'react'

const useMouse = () => {
  const [x, setX] = useState(null)
  const [y, setY] = useState(null)

  useEffect(() => {
    const handleMouseMove = (e) => {
      setX(e.clientX)
      setY(e.clientY)
    }

    document.addEventListener('mousemove', handleMouseMove)
    return () => {
      document.removeEventListener('mousemove', handleMouseMove)
    }
  }, [])

  return { x, y }
}

export default useMouse

The consumer only needs one line:

function App() {
  const { x, y } = useMouse()
  return <div>{x && y ? `Coordinates: ${x}, ${y}` : 'Please move mouse'}</div>
}

Why It Must Start with use

React identifies "this is a Hook" by the function name. Without starting with use, React treats it as a regular function, and the useState inside will directly throw an error. This is both a convention and a runtime rule.

Easy to Confuse

Hooks cannot be nested inside regular functions; they must be written directly in the Hook function body.

Wrong approach:

const useMouse = () => {
  function setup() {            // Regular function
    const [x, setX] = useState()  // Error!
  }
  setup()
}

Reason: React relies on the call order of Hooks to match each state. During the first render, useState, useState, useEffect execute in order; the second render must be exactly the same. If Hooks are placed in conditions or nested functions, the order may change, and React can't tell who is who.

Usage Boundaries

Differences between custom Hooks and regular functions:

Regular Function Custom Hook
Pure computation, input→output Can use useState, useEffect inside
No reactivity Data changes cause consuming components to re-render
No naming restrictions Must start with use

Self-Test

  1. Why must custom Hooks start with use?
  2. Why can't Hooks be placed in conditional statements or nested functions?

Reference Answers

  1. React identifies Hooks by function name; only those starting with use are checked against hooks calling rules.
  2. React matches state by Hooks call order. Conditions or nested functions may change the order, preventing React from establishing correct correspondence.

mousemove High-Frequency Rendering

Knowledge Points

The mousemove event fires dozens to hundreds of times per second; each setX / setY triggers a render. If the data needs to be displayed on the UI, rendering is necessary; if it only needs to be stored temporarily, use useRef to avoid rendering.

Key Code

React merges multiple setStates within the same event callback into a single render:

const handleMouseMove = (e) => {
  setX(e.clientX)   // ─── Merged into one render
  setY(e.clientY)   // ───
}

If you don't need to display coordinates, use useRef:

const useMouse = () => {
  const pos = useRef({ x: 0, y: 0 })

  useEffect(() => {
    const handleMouseMove = (e) => {
      pos.current = { x: e.clientX, y: e.clientY }
      // No setState called, zero renders
    }
    document.addEventListener('mousemove', handleMouseMove)
    return () => document.removeEventListener('mousemove', handleMouseMove)
  }, [])

  return pos
}

Why It's Designed This Way

useState useRef
Does changing it cause render? Yes No
Suitable for what Data to display on UI Data only for temporary storage, computation
Examples Lists, forms, toggles DOM references, timer IDs, coordinates

Selection criteria: use useState if display is needed, useRef if only background computation is needed.

Usage Boundaries

High-frequency events (mousemove, scroll, resize) require special attention to performance. Sixty renders per second won't cause visible lag for simple components, but if the component tree is complex and unoptimized, it could become a bottleneck.

Self-Test

  1. How many renders do two setStates in the same event callback trigger?
  2. When should you use useRef instead of useState to store mouse coordinates?

Reference Answers

  1. One. React automatically merges multiple setStates within the same event callback.
  2. When coordinates don't need to be displayed on the UI, only stored temporarily for computation (e.g., sending to WebSocket).

useEffect Cleanup Function

Knowledge Points

useEffect's callback function can return a cleanup function, executed before the component unmounts. This is React's "borrow-and-return" mechanism for native APIs (event listeners, timers, Worker threads).

Key Code

useEffect(() => {
  document.addEventListener('mousemove', handleMouseMove)  // Borrow

  return () => {
    document.removeEventListener('mousemove', handleMouseMove)  // Return
  }
}, [])

Execution Process

Component mounts → Execute effect callback → addEventListener binds
Component running → Works normally
Component unmounts → Execute cleanup function → removeEventListener unbinds

Why It's Designed This Way

After a component unmounts, its bound event listeners are still in memory. React manages its own virtual DOM, but doesn't manage native APIs like DOM's addEventListener, setInterval, new Worker(), etc. These resources must be manually reclaimed; otherwise, each component mount/unmount cycle adds another leak, and the page gets slower and slower.

Resources needing cleanup:

addEventListener    → removeEventListener
setInterval         → clearInterval
new Worker()        → worker.terminate()

Common Pitfalls

Functions defined with const placed after useEffect—although it works here (effect callback executes after render, by then the function is initialized), it's a bad habit. It's recommended to place function definitions before useEffect.

Self-Test

  1. When does useEffect's cleanup function execute?
  2. What are the consequences of not cleaning up event listeners?

Reference Answers

  1. It executes before the component unmounts, used to clean up resources created in the effect.
  2. Listeners keep occupying memory; repeated component mount/unmount → memory leak → performance degradation.

Code Tied Together

Taking App2.jsx as an example, the complete chain:

User clicks button
  → onClick triggers setTheme('dark')
    → App re-executes, theme becomes 'dark'
    → <Provider value="dark"> broadcasts new value
    → useTheme() in Page perceives change, Page re-renders
    → useTheme() in Child perceives change, Child re-renders
    → Page displays new theme

useMouse chain:

User moves mouse
  → Browser triggers mousemove event
    → handleMouseMove executes → setX + setY
      → React merges into one render
        → App re-renders → Page displays new coordinates

Final Review

Context solves cross-level component communication; Provider is a local container; useContext is subscription consumption. Encapsulating custom Hooks makes logic reusable and validatable. High-frequency events require distinguishing useState from useRef: render if display is needed, ref if not. useEffect cleanup function is "borrow must return". Hooks must be written directly at the top level of Hook functions, not nested.

Self-Check Checklist