Context + 自定义 Hook 最佳模式
标准、专业、可复用的写法 - 前端项目里最常用的模式,适合在真实项目中直接使用。
📚 目录
模式概述
核心思想
将 Context 与自定义 Hook 分离,实现关注点分离:
- Context:只负责创建和分发数据
- 自定义 Hook(Controller):封装所有状态逻辑
- Provider:纯粹的数据传递层
- 自定义 Hook(Consumer):封装消费逻辑和错误处理
架构图
┌─────────────────────────────────────────┐
│ 1. createContext │
│ 创建 Context 对象 │
└──────────────┬──────────────────────────┘
│
↓
┌─────────────────────────────────────────┐
│ 2. useThemeController (自定义 Hook) │
│ 封装所有状态逻辑 │
│ - useState │
│ - useCallback │
│ - useMemo │
└──────────────┬──────────────────────────┘
│
↓
┌─────────────────────────────────────────┐
│ 3. ThemeProvider │
│ 把 controller 返回值传给 Context │
│ - 非常"干净",只负责传递 │
└──────────────┬──────────────────────────┘
│
↓
┌─────────────────────────────────────────┐
│ 4. useTheme (消费 Hook) │
│ 封装 useContext + 错误处理 │
│ - 提供更好的使用体验 │
└──────────────┬──────────────────────────┘
│
↓
┌─────────────────────────────────────────┐
│ 5. 组件中使用 │
│ const { theme, toggleTheme } = │
│ useTheme() │
└─────────────────────────────────────────┘
完整实现步骤
✅ 步骤 1:创建 Context
import { createContext } from 'react';
export const ThemeContext = createContext();要点:
- 只创建 Context,不做其他事情
- 推荐不设置默认值(使用
undefined),便于错误检查
✅ 步骤 2:自定义 Hook 封装状态逻辑
关键:把任何复杂逻辑都放进自定义 Hook,这样 Context 的 Provider 就非常”干净”。
import { useState, useCallback } from 'react';
export function useThemeController() {
const [theme, setTheme] = useState('light');
const toggleTheme = useCallback(() => {
setTheme(t => (t === 'light' ? 'dark' : 'light'));
}, []);
return { theme, toggleTheme };
}要点:
- 所有状态管理逻辑都在这里
- 使用
useCallback稳定函数引用 - 返回需要传递给 Context 的值
✅ 步骤 3:Provider - 把自定义 Hook 的返回值传给 Context
import { ThemeContext } from './ThemeContext';
import { useThemeController } from './useThemeController';
export function ThemeProvider({ children }) {
const controller = useThemeController();
return (
<ThemeContext.Provider value={controller}>
{children}
</ThemeContext.Provider>
);
}要点:
- Provider 非常”干净”,只负责传递
- 直接使用 controller 的返回值,无需额外处理
- 逻辑全部在
useThemeController中
✅ 步骤 4:再封一个自定义 Hook 用于消费 Context(推荐做法)
这样使用体验更好,不需要每次都 useContext:
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) {
throw new Error('useTheme 必须在 ThemeProvider 中使用');
}
return ctx;
}要点:
- 封装
useContext调用 - 提供清晰的错误提示
- 使用体验更好:
useTheme()比useContext(ThemeContext)更语义化
✅ 步骤 5:在组件中使用
import { useTheme } from './useTheme';
function Home() {
const { theme, toggleTheme } = useTheme();
return (
<div>
<p>当前主题:{theme}</p>
<button onClick={toggleTheme}>切换主题</button>
</div>
);
}模式优势
🔥 这种写法的优势
-
逻辑全部在自定义 Hook 中,Provider 更纯粹
- Provider 只负责传递数据,不包含任何业务逻辑
- 代码更清晰,职责分明
-
Context 只负责分发数据,不负责逻辑
- Context 是纯粹的数据通道
- 逻辑和状态管理都在自定义 Hook 中
-
组件消费时体验非常好:
useTheme()- 语义化命名,代码可读性强
- 不需要直接使用
useContext(ThemeContext)
-
可扩展性极强:支持 reducer、API 请求、持久化等
- 可以在
useThemeController中添加任何复杂逻辑 - 支持异步操作、副作用处理等
- 可以在
-
错误处理统一
- 在消费 Hook 中统一处理 Context 未找到的情况
- 提供清晰的错误提示
-
易于测试
- Controller Hook 可以独立测试
- Provider 逻辑简单,测试成本低
实际应用示例
示例 1:主题切换(完整代码)
// ========== ThemeContext.js ==========
import { createContext } from 'react';
export const ThemeContext = createContext();
// ========== useThemeController.js ==========
import { useState, useCallback } from 'react';
export function useThemeController() {
const [theme, setTheme] = useState('light');
const toggleTheme = useCallback(() => {
setTheme(t => (t === 'light' ? 'dark' : 'light'));
}, []);
return { theme, toggleTheme };
}
// ========== ThemeProvider.js ==========
import { ThemeContext } from './ThemeContext';
import { useThemeController } from './useThemeController';
export function ThemeProvider({ children }) {
const controller = useThemeController();
return (
<ThemeContext.Provider value={controller}>
{children}
</ThemeContext.Provider>
);
}
// ========== useTheme.js ==========
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) {
throw new Error('useTheme 必须在 ThemeProvider 中使用');
}
return ctx;
}
// ========== App.js ==========
import { ThemeProvider } from './ThemeProvider';
import { Home } from './Home';
function App() {
return (
<ThemeProvider>
<Home />
</ThemeProvider>
);
}
// ========== Home.js ==========
import { useTheme } from './useTheme';
function Home() {
const { theme, toggleTheme } = useTheme();
return (
<div>
<p>当前主题:{theme}</p>
<button onClick={toggleTheme}>切换主题</button>
</div>
);
}示例 2:用户认证(带异步操作)
// ========== AuthContext.js ==========
import { createContext } from 'react';
export const AuthContext = createContext();
// ========== useAuthController.js ==========
import { useState, useCallback, useEffect } from 'react';
export function useAuthController() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 检查登录状态
checkAuth().then(user => {
setUser(user);
setLoading(false);
});
}, []);
const login = useCallback(async (email, password) => {
setLoading(true);
try {
const user = await authenticate(email, password);
setUser(user);
} finally {
setLoading(false);
}
}, []);
const logout = useCallback(() => {
setUser(null);
}, []);
return {
user,
loading,
login,
logout,
isAuthenticated: !!user
};
}
// ========== AuthProvider.js ==========
import { AuthContext } from './AuthContext';
import { useAuthController } from './useAuthController';
export function AuthProvider({ children }) {
const controller = useAuthController();
return (
<AuthContext.Provider value={controller}>
{children}
</AuthContext.Provider>
);
}
// ========== useAuth.js ==========
import { useContext } from 'react';
import { AuthContext } from './AuthContext';
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) {
throw new Error('useAuth 必须在 AuthProvider 中使用');
}
return ctx;
}
// ========== 使用 ==========
function LoginPage() {
const { login, loading } = useAuth();
// ...
}
function Dashboard() {
const { user, logout } = useAuth();
// ...
}示例 3:带持久化的主题(localStorage)
// ========== useThemeController.js ==========
import { useState, useCallback, useEffect } from 'react';
export function useThemeController() {
const [theme, setTheme] = useState(() => {
// 从 localStorage 恢复
return localStorage.getItem('theme') || 'light';
});
const toggleTheme = useCallback(() => {
setTheme(t => {
const newTheme = t === 'light' ? 'dark' : 'light';
localStorage.setItem('theme', newTheme);
return newTheme;
});
}, []);
return { theme, toggleTheme };
}TypeScript 版本
完整 TypeScript 实现
// ========== ThemeContext.ts ==========
import { createContext } from 'react';
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
export const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// ========== useThemeController.ts ==========
import { useState, useCallback } from 'react';
export function useThemeController(): ThemeContextType {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = useCallback(() => {
setTheme(t => (t === 'light' ? 'dark' : 'light'));
}, []);
return { theme, toggleTheme };
}
// ========== ThemeProvider.tsx ==========
import { ReactNode } from 'react';
import { ThemeContext } from './ThemeContext';
import { useThemeController } from './useThemeController';
interface ThemeProviderProps {
children: ReactNode;
}
export function ThemeProvider({ children }: ThemeProviderProps) {
const controller = useThemeController();
return (
<ThemeContext.Provider value={controller}>
{children}
</ThemeContext.Provider>
);
}
// ========== useTheme.ts ==========
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
export function useTheme(): ThemeContextType {
const ctx = useContext(ThemeContext);
if (!ctx) {
throw new Error('useTheme 必须在 ThemeProvider 中使用');
}
return ctx;
}
// ========== 使用 ==========
function Home() {
const { theme, toggleTheme } = useTheme(); // TypeScript 自动推断类型
// ...
}扩展场景
场景 1:使用 useReducer
// ========== useThemeController.js ==========
import { useReducer, useCallback } from 'react';
function themeReducer(state, action) {
switch (action.type) {
case 'TOGGLE':
return state === 'light' ? 'dark' : 'light';
case 'SET':
return action.payload;
default:
return state;
}
}
export function useThemeController() {
const [theme, dispatch] = useReducer(themeReducer, 'light');
const toggleTheme = useCallback(() => {
dispatch({ type: 'TOGGLE' });
}, []);
const setTheme = useCallback((newTheme) => {
dispatch({ type: 'SET', payload: newTheme });
}, []);
return { theme, toggleTheme, setTheme };
}场景 2:多个状态组合
// ========== useAppController.js ==========
import { useState, useCallback } from 'react';
export function useAppController() {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState(null);
const [language, setLanguage] = useState('zh');
const toggleTheme = useCallback(() => {
setTheme(t => (t === 'light' ? 'dark' : 'light'));
}, []);
const updateUser = useCallback((userData) => {
setUser(userData);
}, []);
const changeLanguage = useCallback((lang) => {
setLanguage(lang);
}, []);
return {
theme,
toggleTheme,
user,
updateUser,
language,
changeLanguage
};
}场景 3:API 请求集成
// ========== useDataController.js ==========
import { useState, useCallback, useEffect } from 'react';
export function useDataController() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchData();
}, []);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await fetch('/api/data').then(r => r.json());
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, []);
const refetch = useCallback(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch };
}文件组织建议
推荐的文件结构
contexts/
├── theme/
│ ├── ThemeContext.js # Context 定义
│ ├── useThemeController.js # 状态逻辑 Hook
│ ├── ThemeProvider.jsx # Provider 组件
│ ├── useTheme.js # 消费 Hook
│ └── index.js # 统一导出
├── auth/
│ ├── AuthContext.js
│ ├── useAuthController.js
│ ├── AuthProvider.jsx
│ ├── useAuth.js
│ └── index.js
└── index.js # 导出所有 Context
统一导出示例
// contexts/theme/index.js
export { ThemeContext } from './ThemeContext';
export { useThemeController } from './useThemeController';
export { ThemeProvider } from './ThemeProvider';
export { useTheme } from './useTheme';
// 使用
import { ThemeProvider, useTheme } from './contexts/theme';最佳实践总结
✅ 推荐做法
-
分离关注点
- Context 只负责创建
- Controller Hook 负责逻辑
- Provider 只负责传递
- Consumer Hook 负责消费和错误处理
-
使用 useCallback 稳定函数引用
const toggleTheme = useCallback(() => { setTheme(t => (t === 'light' ? 'dark' : 'light')); }, []); -
提供清晰的错误提示
if (!ctx) { throw new Error('useTheme 必须在 ThemeProvider 中使用'); } -
语义化命名
- Controller Hook:
useThemeController - Consumer Hook:
useTheme - Provider:
ThemeProvider
- Controller Hook:
❌ 避免的做法
-
不要在 Provider 中写业务逻辑
// ❌ 不好 function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); // 很多业务逻辑... return <ThemeContext.Provider value={...}>{children}</ThemeContext.Provider>; } // ✅ 好 function ThemeProvider({ children }) { const controller = useThemeController(); return <ThemeContext.Provider value={controller}>{children}</ThemeContext.Provider>; } -
不要直接使用 useContext
// ❌ 不好 function Component() { const theme = useContext(ThemeContext); // ... } // ✅ 好 function Component() { const { theme } = useTheme(); // ... }