1. 为什么需要自定义 Hook
在 React 生态中,Hook 已经成为函数组件开发的事实标准。但很多开发者在使用基础 Hook(如 useState、useEffect)时,常常会遇到一个困境:相似的逻辑在不同组件中重复出现,导致代码冗余且难以维护。
我曾在维护一个电商后台系统时深有体会。当时有 20 多个组件需要处理表单验证逻辑,每个组件都写满了类似的 useState 和 useEffect。当业务规则变更时,我们不得不在几十个文件中做相同修改,这简直是维护噩梦。
自定义 Hook 的本质是逻辑复用单元。它允许你将组件逻辑提取到可重用的函数中,这与高阶组件或 render props 等模式不同,不会增加组件层级。通过自定义 Hook,我们可以:
- 将分散的状态逻辑集中管理
- 保持组件的简洁性
- 实现跨组件甚至跨项目的逻辑共享
- 更容易进行单元测试
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 自定义 Hook 的设计原则
2.1 单一职责原则
一个好的自定义 Hook 应该像 Unix 哲学倡导的那样:只做好一件事。比如 useLocalStorage 只处理本地存储逻辑,useFetch 只关心数据获取。
我曾见过一个反例:有人写了个 useUser 的 Hook,既处理用户认证,又管理用户资料,还负责权限检查。当其中某部分逻辑需要修改时,牵一发而动全身。
2.2 明确的输入输出
自定义 Hook 应该像纯函数一样,有清晰的输入参数和返回值。避免使用可变参数或隐式依赖。
javascript复制// 好的设计
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(c => c + 1);
return { count, increment };
}
// 不好的设计 - 依赖外部变量
let externalVar = 10;
function useBadCounter() {
const [count, setCount] = useState(externalVar);
// ...
}
2.3 命名约定
遵循 useXxx 的命名约定不仅是一种规范,还能帮助 lint 工具识别 Hook 规则。我曾遇到一个团队用 get 前缀命名 Hook,导致 ESLint 无法正确检查 Hook 规则,最终引发了难以追踪的 bug。
3. 常见自定义 Hook 模式
3.1 状态管理 Hook
这类 Hook 封装了状态逻辑,比如:
javascript复制function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []);
return [value, toggle];
}
// 使用示例
function Component() {
const [isOn, toggleIsOn] = useToggle();
return <button onClick={toggleIsOn}>{isOn ? 'ON' : 'OFF'}</button>;
}
在实际项目中,我经常扩展这个 Hook 来处理更复杂的状态切换场景,比如添加防抖或异步切换逻辑。
3.2 副作用 Hook
封装副作用逻辑是自定义 Hook 的另一个常见用途:
javascript复制function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
在实现这个 Hook 时,我踩过一个坑:忘记在 cleanup 函数中移除事件监听器,导致内存泄漏。这也是为什么在自定义 Hook 中正确处理副作用如此重要。
3.3 数据获取 Hook
数据获取是前端开发中最常见的需求之一:
javascript复制function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url, options);
const json = await response.json();
setData(json);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, [url, options]);
return { data, error, loading };
}
在实际项目中,我会进一步扩展这个 Hook 来处理缓存、重试逻辑和请求取消等场景。
4. 高级 Hook 设计模式
4.1 组合 Hook
就像组合函数一样,我们可以组合多个 Hook 来创建更强大的抽象:
javascript复制function useUserProfile(userId) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
const [isAdmin, toggleAdmin] = useToggle(user?.isAdmin || false);
const updateProfile = useCallback(async (updates) => {
// 更新逻辑
}, [userId]);
return {
user,
isAdmin,
toggleAdmin,
updateProfile,
loading,
error
};
}
这种模式特别适合复杂业务场景。在我的一个项目管理应用中,通过组合 5 个基础 Hook,我们大大简化了任务看板组件的逻辑。
4.2 依赖注入 Hook
有时我们需要 Hook 具有更强的灵活性:
javascript复制function useApi(apiClient) {
const [data, setData] = useState(null);
const fetchData = useCallback(async (endpoint) => {
const result = await apiClient.get(endpoint);
setData(result);
}, [apiClient]);
return { data, fetchData };
}
// 使用时可注入不同的 apiClient
const { data } = useApi(axios);
// 或者
const { data } = useApi(myCustomClient);
这种模式在测试时特别有用,可以轻松注入 mock 客户端。
4.3 上下文 Hook
将 Hook 与 Context API 结合可以创建强大的状态共享方案:
javascript复制const AuthContext = createContext();
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = useCallback((credentials) => {
// 登录逻辑
}, []);
const value = { user, login };
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
在大型应用中,这种模式可以避免 prop drilling 问题。我曾在企业级应用中用这种模式管理复杂的权限系统。
5. 自定义 Hook 的最佳实践
5.1 测试策略
自定义 Hook 应该像其他业务逻辑一样被充分测试。使用 @testing-library/react-hooks 可以方便地测试 Hook:
javascript复制import { renderHook } from '@testing-library/react-hooks';
import { useCounter } from './useCounter';
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
在我的团队中,我们要求每个自定义 Hook 都必须有至少 80% 的测试覆盖率。
5.2 性能优化
自定义 Hook 中常见的性能问题包括:
- 不必要的重新渲染:使用 useMemo/useCallback 避免在每次渲染时创建新引用
- 过度订阅:确保事件监听器、订阅等在 cleanup 时被正确移除
- 深层比较:对于复杂对象,考虑使用 useDeepCompareEffect 等方案
5.3 文档与类型
良好的文档和类型定义对 Hook 的可维护性至关重要:
typescript复制interface UseCounterReturn {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
/**
* 一个简单的计数器 Hook
* @param initialValue 初始计数值
* @returns {UseCounterReturn} 包含计数器和操作方法的对象
*/
function useCounter(initialValue: number = 0): UseCounterReturn {
// 实现...
}
在我的项目中,我们使用 JSDoc + TypeScript 的组合来确保 Hook 的类型安全。
6. 常见问题与解决方案
6.1 Hook 执行顺序问题
Hook 必须在组件的顶层无条件调用。我曾遇到一个棘手的 bug:有人在条件语句中调用 Hook,导致 Hook 执行顺序不一致。
解决方案:
- 使用 ESLint 的 react-hooks/rules-of-hooks 规则
- 如果需要条件逻辑,将条件移到 Hook 内部
6.2 过时的闭包
这是 Hook 中最常见的问题之一:
javascript复制function useInterval(callback, delay) {
useEffect(() => {
const id = setInterval(() => {
callback(); // 这里可能调用过时的 callback
}, delay);
return () => clearInterval(id);
}, [delay]); // 缺少 callback 依赖
}
解决方案是使用 ref 来保持最新引用:
javascript复制function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
savedCallback.current();
}
const id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
6.3 Hook 之间的交互
当多个 Hook 相互依赖时,要注意避免循环依赖。在我的一个项目中,useAuth 依赖 useApi,而 useApi 又需要 useAuth 提供的 token,这导致了初始化问题。
解决方案是使用依赖注入或分层设计,确保 Hook 之间有清晰的依赖方向。
7. 企业级应用中的 Hook 设计
在大型项目中,自定义 Hook 的设计需要考虑更多因素:
7.1 错误处理
健壮的 Hook 应该提供统一的错误处理机制:
javascript复制function useSafeAsync(fn, deps = []) {
const [state, setState] = useState({
data: null,
error: null,
status: 'idle',
});
const run = useCallback(async (...args) => {
setState(s => ({...s, status: 'pending'}));
try {
const data = await fn(...args);
setState({ data, error: null, status: 'resolved' });
return data;
} catch (error) {
setState({ data: null, error, status: 'rejected' });
throw error;
}
}, deps);
return { ...state, run };
}
7.2 中间件模式
借鉴 Redux 中间件思想,我们可以为 Hook 添加插件机制:
javascript复制function useEnhancedHook(initialValue, ...middlewares) {
const [value, setValue] = useState(initialValue);
let enhancedSetValue = setValue;
middlewares.forEach(middleware => {
enhancedSetValue = middleware(enhancedSetValue);
});
return [value, enhancedSetValue];
}
7.3 性能监控
在生产环境中,Hook 的性能监控很重要:
javascript复制function useProfiledHook(hook, name) {
const startTime = useRef(performance.now());
useEffect(() => {
const duration = performance.now() - startTime.current;
trackHookPerformance(name, duration);
}, []);
return hook();
}
在我的团队中,我们使用类似方案监控关键业务 Hook 的执行时间。
