在 React 16.8 引入 Hooks 之后,我们告别了 Class 组件中复杂的生命周期和高阶组件(HOC)的嵌套地狱。然而,随着业务复杂度的提升,简单的 useState 和 useEffect 组合往往导致组件内部逻辑臃肿,难以维护。
很多开发者停留在"把逻辑抽离成函数"的初级阶段,却忽略了自定义 Hooks(Custom Hooks)本质上是逻辑复用的设计模式。本文将深入探讨自定义 Hooks 的高级设计模式,如何通过合理的抽象提升代码的可读性、可测试性和复用性。
一、为什么我们需要高级设计模式?
在初级实践中,我们常看到这样的代码:
scss
// ❌ 反模式:逻辑泄露与耦合
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/user/${userId}`).then(setUser).finally(() => setLoading(false));
}, [userId]);
// ... 还有获取用户帖子、获取用户关注列表的逻辑混在一起
return loading ? <Spinner /> : <div>{user.name}</div>;
}
这种写法的问题在于:
- UI 与逻辑耦合:组件既负责渲染,又负责数据获取。
- 难以测试:很难在不渲染 UI 的情况下测试数据获取逻辑。
- 无法复用:如果在另一个页面也需要获取用户信息,代码只能复制粘贴。
通过自定义 Hooks,我们可以将"关注点分离(Separation of Concerns)"。
二、核心设计模式详解
2.1 容器模式(Container Pattern)的 Hooks 化
这是最经典的模式,将数据获取和状态管理逻辑剥离,组件只负责展示。
javascript
// ✅ useUser.ts - 专注数据逻辑
export function useUser(userId) {
const [state, setState] = useState({ data: null, loading: true, error: null });
useEffect(() => {
let cancelled = false;
async function fetchUser() {
try {
const response = await fetch(`/api/user/${userId}`);
if (!cancelled) {
setState({ data: await response.json(), loading: false, error: null });
}
} catch (err) {
if (!cancelled) setState({ data: null, loading: false, error: err });
}
}
fetchUser();
return () => { cancelled = true; }; // 清理副作用
}, [userId]);
return state;
}
// ✅ UserProfile.tsx - 专注 UI 展示
function UserProfile({ userId }) {
const { data: user, loading, error } = useUser(userId);
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <div>{user.name}</div>;
}
优势:UI 组件变得极其纯净,逻辑 Hook 可以独立进行单元测试。
2.2 状态机模式(State Machine Pattern)
对于复杂的交互流程(如表单提交、多步骤向导、播放器控制),简单的布尔值状态(isLoading, isSuccess, isError)容易导致状态冲突。此时应引入有限状态机思想。
php
// ✅ useAsyncAction.ts - 管理复杂状态流转
function useAsyncAction(asyncFunction) {
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'START': return { status: 'loading', data: null, error: null };
case 'SUCCESS': return { status: 'success', data: action.payload, error: null };
case 'FAILURE': return { status: 'failure', data: null, error: action.payload };
case 'RESET': return { status: 'idle', data: null, error: null };
default: return state;
}
}, { status: 'idle', data: null, error: null });
const execute = useCallback(async (...args) => {
dispatch({ type: 'START' });
try {
const result = await asyncFunction(...args);
dispatch({ type: 'SUCCESS', payload: result });
} catch (err) {
dispatch({ type: 'FAILURE', payload: err });
}
}, [asyncFunction]);
return { ...state, execute };
}
应用场景:登录注册流程、文件上传、复杂的表单验证。它保证了状态流转的确定性,避免了"既 loading 又 error"的非法状态。
2.3 组合模式(Composition Pattern)
Hooks 最大的威力在于组合。我们可以像搭积木一样,将多个小 Hooks 组合成一个功能强大的大 Hook。
scss
// 基础 Hook:处理本地存储
function useLocalStorage(key, initialValue) {
// ... 实现略
return [value, setValue];
}
// 基础 Hook:处理窗口大小
function useWindowSize() {
// ... 实现略
return { width, height };
}
// ✅ 组合 Hook:响应式主题管理器
function useResponsiveTheme() {
const [theme, setTheme] = useLocalStorage('app-theme', 'light');
const { width } = useWindowSize();
// 自动逻辑:屏幕小于 768px 强制使用移动端样式,但保留用户主题偏好
const isMobile = width < 768;
const effectiveTheme = isMobile ? 'mobile-optimized' : theme;
useEffect(() => {
document.body.className = effectiveTheme;
}, [effectiveTheme]);
return { theme, setTheme, isMobile };
}
核心价值:降低了单个 Hook 的认知负荷,每个 Hook 只做一件事,并通过组合产生新的行为。
2.4 观察者模式与订阅机制
在处理全局事件或非 React 源的数据(如 WebSocket、第三方 SDK)时,可以使用观察者模式。
javascript
// ✅ useWebSocket.ts
function useWebSocket(url) {
const [message, setMessage] = useState(null);
useEffect(() => {
const ws = new WebSocket(url);
ws.onmessage = (event) => {
setMessage(JSON.parse(event.data));
};
ws.onerror = (error) => {
console.error('WS Error', error);
};
// 清理连接
return () => {
ws.close();
};
}, [url]);
const sendMessage = useCallback((data) => {
// 发送逻辑
}, []);
return { message, sendMessage };
}
三、避坑指南:自定义 Hooks 的常见陷阱
3.1 条件调用 Hooks
错误示范:
scss
function useConditionalHook(condition) {
if (condition) {
useEffect(() => { ... }); // ❌ 违反 Rules of Hooks
}
}
修正:Hooks 必须在顶层调用。如果需要根据条件执行逻辑,请将条件判断写在 Hook 内部,而不是包裹 Hook 本身。
3.2 过度抽象
不要为了复用而复用。如果一个逻辑只在当前组件使用,或者不同组件的使用差异极大,强行提取 Hook 反而会增加认知负担。 "三次法则" 是一个不错的经验:当同一段逻辑出现第三次时,再考虑提取。
3.3 依赖项数组的陷阱
在自定义 Hook 中返回回调函数时,务必注意闭包陷阱。
scss
// ❌ 容易捕获旧状态的回调
function useCounter() {
const [count, setCount] = useState(0);
const logCount = () => {
console.log(count); // 可能永远是初始值或旧值
};
return { count, logCount };
}
// ✅ 使用 ref 或将其放入 useEffect/useCallback 依赖中
function useCounter() {
const [count, setCount] = useState(0);
const logCount = useCallback(() => {
console.log(count);
}, [count]); // 确保依赖最新 count
return { count, logCount };
}
四、实战案例:构建一个通用的 useFetch
结合上述模式,我们来构建一个生产级别的 useFetch。
javascript
import { useEffect, useReducer, useCallback } from 'react';
// 定义状态类型
const initialState = {
data: null,
loading: false,
error: null,
};
function reducer(state, action) {
switch (action.type) {
case 'REQUEST': return { ...state, loading: true, error: null };
case 'SUCCESS': return { loading: false, data: action.payload, error: null };
case 'FAILURE': return { loading: false, data: null, error: action.payload };
case 'RESET': return initialState;
default: return state;
}
}
export function useFetch(url, options = {}) {
const [state, dispatch] = useReducer(reducer, initialState);
const { manual = false } = options; // 是否手动触发
const execute = useCallback(async (overrideUrl) => {
const targetUrl = overrideUrl || url;
if (!targetUrl) return;
dispatch({ type: 'REQUEST' });
try {
const response = await fetch(targetUrl);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
dispatch({ type: 'SUCCESS', payload: data });
} catch (err) {
dispatch({ type: 'FAILURE', payload: err.message });
}
}, [url]);
useEffect(() => {
if (!manual) {
execute();
}
}, [execute, manual]);
return { ...state, refetch: execute, reset: () => dispatch({ type: 'RESET' }) };
}
使用示例:
javascript
function UserList() {
const { data, loading, error, refetch } = useFetch('/api/users');
if (loading) return <div>加载中...</div>;
if (error) return <div>出错了: {error} <button onClick={refetch}>重试</button></div>;
return (
<ul>
{data.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
五、总结
自定义 Hooks 不仅仅是代码复用的工具,更是 React 组件架构的核心支柱。
- 逻辑解耦:让 UI 组件回归纯粹的表现层。
- 状态治理:利用 Reducer 和状态机管理复杂交互。
- 能力组合:通过小 Hook 的堆叠构建复杂功能。
- 测试友好:逻辑与视图分离使得单元测试变得简单高效。
掌握这些高级模式,你将能够编写出更健壮、更易维护的 React 应用,真正发挥 Hooks 体系的威力。
后续思考题:
- 如何在自定义 Hook 中处理服务端渲染(SSR)时的 Hydration 问题?
- 自定义 Hooks 能否完全替代 Redux/MobX 等全局状态管理库?边界在哪里?
欢迎在评论区分享你在项目中封装过的最得意的自定义 Hook!