跪拜 Guibai
← Back to the summary

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:

  1. Deep component hierarchies make passing props layer by layer painful
  2. 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:

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:

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:

  1. The essence of useContext: Cross-level lookup of Providers on the Fiber tree, breaking Props Drilling
  2. The essence of Custom Hooks: Reusability of regular functions + Access to React's reactive system
  3. useEffect cleanup function: Browser APIs are outside React's scope; what is created must be manually destroyed
  4. 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 🚀