Context and Custom Hooks Are React's Answer to Prop Drilling and Logic Sprawl
React Context and Custom Hooks from the Ground Up: A Complete Analysis of "Cross-Level Communication + Side Effect Encapsulation"
Foreword
In React development, there are two pain points that almost every developer encounters:
- Deep component hierarchies make passing props layer by layer painful
- Multiple components need to reuse the same logic, and copy-pasting feels too low
React's official solutions are: useContext to solve the first problem, and Custom Hooks to solve the second. Combining the two is the core weapon of React's comprehensive Hooks programming.
This article will use a theme-switching demo and a mouse-tracking demo as examples, starting from the underlying dilemma of component communication, gradually deriving the design motivation of Context, then diving into the encapsulation philosophy of custom Hooks, and finally landing on the underlying principles of useEffect's cleanup mechanism.
After reading, you will understand not just "how to use it," but also "why it was designed this way."
1. The Dilemma of Component Communication — Why Do We Need Context?
1.1 React's Unidirectional Data Flow
React's core design principle is unidirectional data flow: data can only be passed down from parent components to child components via props.
App (holds data)
└─ Parent (forwards props)
└─ Child (forwards props)
└─ GrandChild (finally uses it)
This pattern is fine when there are few levels, but once the component tree gets deep, the Props Drilling problem emerges — intermediate components are forced to act as "porters" for data they don't need, making the code verbose and hard to maintain.
1.2 Classification of Relationships
Relationships between components can be divided into four types:
| Relationship Type | Communication Method | Pain Point |
|---|---|---|
| Parent-Child | Direct props passing | Naturally supported, no pain point |
| Sibling | State lifting to a common parent | Requires extra design |
| Grandparent-Grandchild | Props forwarding layer by layer | Hierarchy is too deep, porting is too painful |
| Strangers | Global state management | Requires external help like Redux/Zustand |
Context was born precisely to solve the communication problems of grandparent-grandchild relationships and deep component trees.
2. Three Steps to useContext — Create, Provide, Consume
Using useContext follows a fixed three-step pattern. Understanding these three steps means understanding the entire Context mechanism.
2.1 Step 1: createContext — Create the Context
// ThemeContext.jsx
import { createContext } from 'react';
// "light" in createContext("light") is the default value
// When there is no Provider in the component tree, useContext returns this default value
export const ThemeContext = createContext("light");
Underlying Understanding: createContext essentially creates a "data pipeline." This pipeline itself doesn't store data; it's just a declaration of a channel — telling React: "I've defined a type of data that can be passed across levels." The returned ThemeContext object carries two key things: Provider (the sending end) and Consumer (the receiving end, but in the Hooks era we use useContext instead).
2.2 Step 2: Provider — Inject Data
// In the root component or any upper-level component
<ThemeContext.Provider value="dark">
<Page />
</ThemeContext.Provider>
Underlying Understanding: Provider is a special component implemented internally by React. When you pass it a value, React stores this value in a linked list on the Fiber node. Each Provider corresponds to a node in the linked list, and there can be multiple Providers of the same type nested in the component tree — the inner one overrides the outer one. This is the underlying principle behind "the nearest Provider takes effect."
2.3 Step 3: useContext — Consume Data
// Page.jsx
import { useContext } from 'react';
import { ThemeContext } from '../ThemeContext';
const Page = () => {
const theme = useContext(ThemeContext); // Returns the value of the nearest Provider
console.log(theme); // "dark"
return (
<>
Page {theme}
<Child />
</>
);
};
Underlying Understanding: During component rendering, useContext traverses up the Fiber tree, finds the nearest Provider of the corresponding type, and retrieves its value to return. This process is independent of component hierarchy — no matter how deeply Page is nested, it can skip all intermediate levels and directly read the Provider's data.
Key Detail: Once the Provider's value changes (reference change), all components using that Context will re-render. React uses Object.is to compare the old and new values, so if you pass an object literal value={{ theme: "dark" }}, a new object is created every time App renders, leading to performance issues — this is one of the most common pitfalls with Context.
3. Custom Hooks — The Power of Encapsulation
3.1 Why Do We Need Custom Hooks?
Suppose you have 10 components that all need to read the theme. Each component has to write:
import { useContext } from 'react';
import { ThemeContext } from '../ThemeContext';
// ...
const theme = useContext(ThemeContext);
Two lines of import + one line of invocation, written 10 times. And if the logic changes later (like adding a theme toggle function), every component needs to be modified — this is shotgun surgery.
3.2 Encapsulating useTheme
// hooks/useTheme.js
import { ThemeContext } from '../ThemeContext';
import { useContext } from 'react';
// Custom Hooks must start with "use" — this is React's convention
// Because React's ESLint plugin relies on this prefix to identify Hooks and enforce rules
export function useTheme() {
return useContext(ThemeContext);
}
Now the component only needs one line:
// Child.jsx
import { useTheme } from '../hooks/useTheme';
function Child() {
const theme = useTheme(); // Clean and neat
return <button className={theme}>Button {theme}</button>;
}
3.3 The Essence of Custom Hooks
Many people ask: What is the difference between a custom Hook and a regular function?
The answer lies in its ability to "contain" React's reactive system:
| Regular Function | Custom Hook | |
|---|---|---|
| Can encapsulate logic | ✅ | ✅ |
| Can call useState | ❌ (Violates Hook rules) | ✅ |
| Can call useEffect | ❌ | ✅ |
| Can call other Hooks | ❌ | ✅ |
| Can return reactive data | ❌ | ✅ |
Custom Hook = Reusability of a regular function + Access to React's reactive system.
It packages "reactive state + side effects + business logic" into a black box, exposing only the simplest interface to the outside. This is what the notes mean by "compared to regular function encapsulation, the extra part is the ability to encapsulate React's reactivity, side effects, and business logic."
4. Practical Example: useMouse — Encapsulating Mouse Tracking Logic
The useTheme above is just a simple "wrapper" — a single line of useContext. The true power of custom Hooks is demonstrated in encapsulating complex logic that includes side effects.
4.1 The Complete useMouse Hook
// hooks/useMouse.js
import { useState, useEffect } from 'react';
export const useMouse = () => {
// Reactive state: mouse coordinates
const [x, setX] = useState(null);
const [y, setY] = useState(null);
// Mouse move callback
const handleMouseMove = (e) => {
setX(e.clientX);
setY(e.clientY);
};
useEffect(() => {
// Component mount: bind event listener
document.addEventListener('mousemove', handleMouseMove);
// Component unmount: clean up side effect
return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
}, []); // Empty dependency array = only execute on mount/unmount
return { x, y };
};
4.2 Using It in a Component
// App.jsx
import { useMouse } from './hooks/useMouse';
function App() {
const { x, y } = useMouse();
return (
<div style={{ height: '100vh', display: 'flex',
alignItems: 'center', justifyContent: 'center' }}>
{x && y ? `x: ${x}, y: ${y}` : 'Mouse not moved'}
</div>
);
}
Look closely: there is no useState, useEffect, or addEventListener anywhere in the component. All the complex logic is locked inside useMouse, and the component only cares about "consuming data."
This is the highest realm of custom Hooks: letting components only care about "what it is," not "how it's implemented."
5. useEffect Cleanup Mechanism — Why Must We Manually Reclaim?
This is a pitfall many beginners easily fall into, and it's one of the most important knowledge points in the notes.
5.1 React Manages the DOM, Not Browser APIs
React's scope of work is Virtual DOM → Real DOM. When you write:
document.addEventListener('mousemove', handleMouseMove);
This line of code calls the browser's native API, stepping outside React's jurisdiction. When React unmounts a component, it removes the corresponding DOM node from the page, but it does not, and cannot, automatically:
- Clear timers (
clearInterval) - Terminate Web Workers (
worker.terminate()) - Unbind event listeners (
removeEventListener)
5.2 Consequences of Not Cleaning Up
| Resource Type | What Happens If Not Cleaned Up |
|---|---|
Timers setInterval |
The component is unmounted, but the timer still runs in the background. If the callback calls setState, React will issue a memory leak warning in the console. |
Event Listeners addEventListener |
The component is gone, but the callback is still held by reference by document/window, the entire closure cannot be GC'd, causing a memory leak. |
| Web Worker | The thread continues to consume CPU resources until the user closes the page. |
| WebSocket/Subscriptions | The connection persists, and upon receiving a push, it tries to update a non-existent component → throws an error directly. |
5.3 useEffect's return — The Cleanup Function
useEffect(() => {
// Side effect: create resource
const timer = setInterval(() => tick(), 1000);
// return a cleanup function
return () => {
// Side effect: destroy resource
clearInterval(timer);
};
}, []);
Underlying Principle: Before re-executing an effect (when dependencies change) or before a component unmounts, React first calls the cleanup function returned from the previous execution. This mechanism ensures that creation and destruction always occur in pairs:
Mount → Execute effect function (create)
Dependency change → First execute the previous cleanup function (destroy), then execute the new effect (create)
Unmount → Execute cleanup function (destroy)
This is what the notes mean by: "After a function component unmounts, it won't actively reclaim. Timers, Workers, events — manually reclaim."
6. Project Directory Structure — Engineering Mindset
Looking back at the entire Demo's directory:
src/
├── hooks/ ← Custom Hooks, belonging to the architecture layer
│ ├── useTheme.js ← Encapsulates Context consumption logic
│ └── useMouse.js ← Encapsulates mouse tracking logic
├── components/ ← UI components, only responsible for rendering
│ ├── Page.jsx
│ └── Child.jsx
├── ThemeContext.jsx ← Context definition
├── App.jsx ← Root component
└── main.jsx ← Entry point
Layering Philosophy:
hooks/is the logic layer — encapsulates all the "how to implement"components/is the view layer — only cares about "what to display"ThemeContext.jsxis the data pipeline — connects Provider and Consumer
This is the core architectural pattern of the React Hooks era: separation of concerns, logic reuse relies on Hooks, UI reuse relies on components.
7. Summary
Starting from the dilemma of component communication, this article derived the following knowledge chain layer by layer:
Component Communication Pain Point → createContext + Provider + useContext (Basic Three Steps)
→ Custom Hook Encapsulating Context (useTheme)
→ Custom Hook Encapsulating Side Effects (useMouse)
→ Underlying Principle of useEffect Cleanup Mechanism
Core Points:
- The essence of useContext: Cross-level lookup of Providers on the Fiber tree, breaking Props Drilling
- The essence of Custom Hooks: Reusability of regular functions + Access to React's reactive system
- useEffect cleanup function: Browser APIs are outside React's scope; what is created must be manually destroyed
- Engineering mindset:
hooks/for logic,components/for views, each performing its own role
If you found this article helpful, feel free to like and bookmark it~ If you have questions, feel free to discuss in the comments 🚀