React Context, Custom Hooks, and the Rendering Traps Between Them
Today's Highlights
- Four relationships of component communication and their applicable boundaries
- Three-step useContext template: createContext → Provider → useContext
- The meaning of encapsulating custom Hooks: reuse, validation, isolation
- The relationship between Context and unidirectional data flow
- Who triggers component rendering, what memo can and cannot block
- The abstraction process of useMouse and the choice for high-frequency mousemove rendering
- useEffect borrow-and-return pattern and Hooks calling rules
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
- Why must sibling component communication go through the parent component?
- What is the essence of state lifting?
Reference Answers
- 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.
- 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
- What is the parameter of createContext? What is its relationship with Provider's value?
- Is Provider global? Why?
Reference Answers
- 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.
- 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
- What is the greatest value of encapsulating useTheme?
- What is the hidden danger of using useContext(ThemeContext) directly without encapsulation?
Reference Answers
- It allows validation before retrieving the value, immediately throwing an error when Provider is forgotten instead of failing silently.
- 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
- Does Context change React's unidirectional data flow?
- Can a child component reversely modify parent component data through Context?
Reference Answers
- No. Data always flows from ancestor to descendant; the flow direction hasn't changed.
- 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
- Page uses useTheme; when App re-renders, will Page definitely re-render? Can React.memo block it?
- If Page doesn't use useTheme but has React.memo added, and Provider value changes, will Page render?
Reference Answers
- Yes. memo cannot block renders triggered by Context subscriptions; Page still renders.
- 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
- Why must custom Hooks start with use?
- Why can't Hooks be placed in conditional statements or nested functions?
Reference Answers
- React identifies Hooks by function name; only those starting with
useare checked against hooks calling rules. - 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
- How many renders do two setStates in the same event callback trigger?
- When should you use useRef instead of useState to store mouse coordinates?
Reference Answers
- One. React automatically merges multiple setStates within the same event callback.
- 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
- When does useEffect's cleanup function execute?
- What are the consequences of not cleaning up event listeners?
Reference Answers
- It executes before the component unmounts, used to clean up resources created in the effect.
- 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
- Can I name the four relationships of component communication?
- Can I write the three-step useContext template?
- Do I know the difference between createContext's default value and Provider value?
- Do I know that Context does not break unidirectional data flow?
- Can I explain whether React.memo is effective for Context-subscribed components?
- Can I write a custom Hook?
- Do I know when to use useState and when to use useRef?
- Do I know when useEffect's cleanup function executes and what it cleans up?
- Do I know why Hooks cannot be nested inside regular functions?
- Can I answer the self-test questions in each chapter?