React Context and Custom Hooks Fix Props Drilling with a One-Line Call
React Context + Custom Hooks: Say Goodbye to Props Hell, Pass Data Across Levels with One Line of Code
Starting from the nightmare of "passing props layer by layer," using two examples of theme switching and mouse tracking to help you thoroughly understand React Context and custom Hooks.
Preface
Have you ever encountered this situation: data is fetched in the outermost component, but the component that actually uses it is a child nested five levels deep? You have to add props passing at every intermediate layer, even if those intermediate components don't care about this data at all.
React Context is here to solve this problem. This article is suitable for those who are new to React, already know how to write useState and parent-child component communication, but haven't figured out how to elegantly implement "cross-level data passing."
After reading, you will gain: understanding the Context trio (createContext / Provider / useContext), learning to encapsulate custom Hooks, and grasping the architectural thinking of a complete project.
Complete runnable project code is attached at the end of the article, which you can directly clone and run to see the effect.
Project Overview
This project demonstrates the usage of Context and custom Hooks with two demos:
| Demo | File | Demonstration Content |
|---|---|---|
| Mouse Tracking | App.jsx + useMouse.js |
Custom Hook encapsulates mouse coordinates |
| Theme Switching | App2.jsx + Theme.Context.jsx + useTheme.js |
Context shares theme across levels |
Component Tree
App
├── ThemeContext.Provider (provides theme data)
│ └── Page (consumes theme, displays current theme value)
│ └── Child (consumes theme, button color changes with theme)
└── button (click to switch light / dark)
Core Technologies Used
createContext— creates a context container.Provider— provides datauseContext— consumes data- Custom Hooks (
useTheme,useMouse) useState+useEffect
Core Knowledge Point 1: Context — A Cross-Level Data Pipeline
Problem: What Does Props Hell Look Like?
Assuming without Context, you want to pass a theme data from App to the deepest Child:
// ❌ Props are carried layer by layer, intermediate components are forced to pass data they don't use
function App() {
const theme = 'dark';
return <Page theme={theme} />;
}
function Page({ theme }) {
return <Child theme={theme} />; // Page itself doesn't use theme at all
}
function Child({ theme }) {
return <button className={theme}>Button</button>; // Only it really needs it
}
In the project's App2.jsx, there is a commented-out piece of code that exactly portrays this "layer-by-layer passing":
// App2.jsx commented code
// <Parent>
// <Child>
// <GrandChild>
// <GreatGrandChild></GreatGrandChild>
// </GrandChild>
// </Child>
// </Parent>
If nested to the fourth or fifth level, each layer has to be a props "porter," making the code messy and error-prone.
Solution: The Context Trio
React Context is like a direct pipeline — data is injected at the outermost layer, and any deeply nested component can read it directly, without intermediate layers needing to participate.
Step 1: createContext Creates the Pipeline
// Theme.Context.jsx
import { createContext } from 'react';
export const ThemeContext = createContext("light");
createContext("light") does two things:
- Creates a context object (pipeline)
"light"serves as the default value — when the component tree is not wrapped by a Provider, this value is used
Step 2: Provider Injects Data
// App2.jsx
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Page />
<button onClick={() =>
setTheme(theme === 'light' ? 'dark' : 'light')
}>Switch Theme</button>
</ThemeContext.Provider>
);
}
Provider is a "data emitter," value={theme} tells it what data to share currently.
Key point: Provider doesn't need to be placed globally — you wrap whichever subtree you want to share data with. Here, ThemeContext.Provider only wraps <Page /> and the button, which is its "scope of influence."
Step 3: useContext Consumes Data
In the project, both Page and Child read the theme directly, without any intermediate layer passing:
// Page.jsx
const Page = () => {
const theme = useTheme(); // Use directly, no need to pass from App
return (
<>
Page {theme}
<br />
<Child /> {/* No theme prop written at all when passing to Child */}
</>
);
};
// Child.jsx
function Child() {
const theme = useTheme(); // Fetch directly!
return <button className={theme}>Button {theme}</button>;
}
Context data flow diagram:
ThemeContext.Provider value="dark"
│
├── Page ──useContext()──→ "dark" ✅ Skips App, reads directly
│ │
│ └── Child ──useContext()──→ "dark" ✅ Skips Page, reads directly
Each layer skips props and directly "reaches out" to fetch from Context. This is the essence of cross-level communication.
Core Knowledge Point 2: Custom Hooks — Encapsulating "Capabilities" into a One-Line Call
What Are They?
A custom Hook is a function starting with use that can call React's built-in Hooks (useState, useEffect, etc.), packaging reactive logic into a reusable "toolbox."
The project encapsulates two custom Hooks:
useTheme: Making Context Consumption More Elegant
// hooks/useTheme.js
import { ThemeContext } from '../Theme.Context.jsx';
import { useContext } from 'react';
export function useTheme() {
return useContext(ThemeContext);
}
Why add this seemingly redundant layer of encapsulation? Isn't writing useContext(ThemeContext) directly in each component enough?
Three reasons:
- Semantic clarity:
useTheme()is more intuitive to read thanuseContext(ThemeContext) - Decoupling: If the Context is switched from
ThemeContexttoReduxorZustandin the future, only theuseTheme.jsfile needs to be changed, and all components remain unaffected - IDE friendly: Typing
useTheme()provides clear autocomplete suggestions and won't be confused with other Contexts
useMouse: Encapsulating Side Effect Logic
This is the project's other demo — tracking mouse coordinates:
// hooks/useMouse.js
import { useState, useEffect } from 'react';
export const useMouse = () => {
const [x, setX] = useState(0);
const [y, setY] = useState(0);
useEffect(() => {
const handleMouseMove = (e) => {
setX(e.clientX);
setY(e.clientY);
};
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
}, []);
return { x, y };
};
Line-by-line breakdown:
| Code | Purpose |
|---|---|
const [x, setX] = useState(0) |
Reactively stores mouse X coordinate, initial value 0 |
const [y, setY] = useState(0) |
Same for Y coordinate |
useEffect(..., []) |
Executes once when component mounts, [] means no dependencies |
document.addEventListener('mousemove', handleMouseMove) |
Listens to global mouse move events |
handleMouseMove |
Updates x, y state on every mouse move |
return () => removeEventListener(...) |
Cleanup function — removes listener when component unmounts, preventing memory leaks |
return { x, y } |
Exposes coordinates externally, letting the caller use them however they want |
Usage is extremely concise:
// App.jsx
function App() {
const { x, y } = useMouse();
return (
<div>
{x && y ? `x:${x}, y:${y}` : 'Mouse not moved'}
</div>
);
}
One line useMouse() handles all state management + event listening + cleanup logic. This is the power of custom Hooks — encapsulating complex reactive logic into a "black box," requiring only a single line call when used.
Knowledge Integration: Context + Custom Hooks = Perfect Partners
Combining the two forms the architectural pattern in this project:
Theme.Context.jsx ← createContext builds the pipeline
↓
hooks/useTheme.js ← Custom Hook encapsulates useContext
↓
components/Page.jsx ← Calls useTheme(), consumes in one line
components/Child.jsx ← Same, completely no props needed
This is React's recommended "logic layer → view layer" separation:
- Context is responsible for the "data pipeline"
- Custom Hooks are responsible for the "logic of consuming data"
- Components are only responsible for "rendering UI"
Summary
Review what you can master from this project:
- Context is a pipeline for sharing data across levels —
createContextbuilds the pipeline,Providerinjects data,useContextfetches data - No need to wrap Provider globally — wrap whichever subtree, data is shared within that scope
- Custom Hooks are essentially logic encapsulation — packaging useState, useEffect, etc., into a one-line-call "black box"
useThemedemonstrates the value of "decoupling" — components don't need to know the specific implementation of ContextuseMousedemonstrates the standard pattern for "side effect management" — three steps: listen, update, clean up; don't forget to return a cleanup function in useEffect
Next Steps for Extension
- Add throttling to
useMouseto reduce setState frequency - Use Context to implement global user login state (
UserContext, providinguserandsetUser) - Learn the
useReducer+ Context combination to handle more complex state logic
Complete Project Code
The project is hosted on Gitee and can be directly cloned and run:
# 1. Clone the repository
git clone [email protected]:dcx2758/ai_doubao_dcx.git
cd ai_doubao_dcx/dcx/fe/react/basic/context-demo/context-demo/
# 2. Install dependencies
npm install
# 3. Start the development server
npm run dev
Project File Tree
context-demo/
├── index.html
├── package.json
├── vite.config.js
├── eslint.config.js
├── src/
│ ├── main.jsx # Entry file
│ ├── App.jsx # Demo1: useMouse mouse tracking
│ ├── App2.jsx # Demo2: Context theme switching (manually change main.jsx import)
│ ├── Theme.Context.jsx # createContext creates theme context
│ ├── index.css # Global styles (CSS variables + dark mode)
│ ├── App.css # App styles
│ ├── hooks/
│ │ ├── useMouse.js # Custom Hook: mouse coordinates
│ │ └── useTheme.js # Custom Hook: consume theme context
│ └── components/
│ ├── Page.jsx # Page component, consumes theme
│ └── Child.jsx # Child component, consumes theme
Dependencies and Versions
| Dependency | Version |
|---|---|
| react | ^19.2.6 |
| react-dom | ^19.2.6 |
| vite | ^8.0.12 |
| @vitejs/plugin-react | ^6.0.1 |
Note: By default,
main.jsximportsApp.jsx(mouse tracking demo). If you want to experience the theme switching demo, changeimport App from './App.jsx'toimport App from './App2.jsx', then observe how Page and Child fetch data across levels on the theme switching page.
What do you think of the Context approach? Have you encountered "props hell" in your projects? Feel free to share in the comments 👏