在react开发中,随着业务复杂度提升,仅依靠内置Hooks往往难以满足需求。本文将系统性地介绍如何设计和封装高质量的自定义Hooks,帮助你提升代码复用性和开发效率。
自定义Hooks本质上是一种逻辑复用机制,它允许你将组件逻辑提取到可重用的函数中。与工具函数不同,自定义Hooks:
可以调用其他Hooks
遵循use前缀命名约定
保持React的声明式特性
单一职责:每个Hook应只解决一个特定问题
明确依赖:清晰定义输入输出接口
性能优化:合理使用useMemo/useCallback避免不必要的计算
类型安全:为TypeScript项目提供完整的类型定义
原始版本存在handler更新不及时的问题,以下是改进实现:
import { useEffect, useRef } from 'react';
const useEventListener = <K extends keyof WindowEventMap>(
eventType: K,
handler: (event: WindowEventMap[K]) => void,
element: Window | htmlElement = window,
options?: boolean | AddEventListenerOptions
) => {
const savedHandler = useRef(handler);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const isSupported = element && element.addEventListener;
if (!isSupported) return;
const eventListener = (event: WindowEventMap[K]) => savedHandler.current(event);
element.addEventListener(eventType, eventListener, options);
return () => {
element.removeEventListener(eventType, eventListener, options);
};
}, [eventType, element, options]);
};
// 使用示例
const WindowResizeDemo = () => {
useEventListener('resize', (e) => {
console.log('Window size:', window.innerWidth, window.innerHeight);
});
return <div>调整浏览器窗口大小查看控制台输出</div>;
};
关键改进:
完整的TypeScript类型支持
支持更多事件目标元素
完善的options参数传递
添加了api可用性检查
针对复杂定时器场景,我们设计更强大的useInterval:
import { useEffect, useRef } from 'react';
const useAdvancedInterval = (
callback: () => void,
delay: number | null,
options?: {
immediate?: boolean; // 是否立即执行
maxTimes?: number; // 最大执行次数
onEnd?: () => void; // 结束时回调
}
) => {
const savedCallback = useRef(callback);
const timesRef = useRef(0);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
let id: NodeJS.Timeout;
const tick = () => {
savedCallback.current();
timesRef.current += 1;
if (options?.maxTimes && timesRef.current >= options.maxTimes) {
clearInterval(id);
options.onEnd?.();
}
};
if (options?.immediate) {
tick(); // 立即执行一次
id = setInterval(tick, delay);
} else {
id = setInterval(tick, delay);
}
return () => {
clearInterval(id);
timesRef.current = 0;
};
}, [delay, options?.immediate, options?.maxTimes, options?.onEnd]);
};
// 九宫格抽奖示例
const LotteryGrid = () => {
const [position, setPosition] = useState(0);
const [speed, setSpeed] = useState(500);
const [isWinning, setIsWinning] = useState(false);
useAdvancedInterval(
() => {
setPosition((prev) => (prev + 1) % 8);
// 加速逻辑
if (!isWinning && speed > 100) {
setSpeed(s => s - 50);
}
},
isWinning ? null : speed,
{ maxTimes: 20 }
);
// ...其他抽奖逻辑
};
高级特性:
支持立即执行模式
可设置最大执行次数
结束回调通知
动态调整间隔时间
import { useState, useEffect, useCallback, useRef } from 'react';
type RequestOptions<T> = {
manual?: boolean;
defaultParams?: any[];
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
formatResult?: (response: any) => T;
};
const useRequest = <T>(
service: (...args: any[]) => Promise<T>,
options: RequestOptions<T> = {}
) => {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);
const paramsRef = useRef(options.defaultParams || []);
const run = useCallback(async (...args: any[]) => {
paramsRef.current = args;
setLoading(true);
try {
let response = await service(...args);
if (options.formatResult) {
response = options.formatResult(response);
}
setData(response);
options.onSuccess?.(response);
} catch (err) {
setError(err as Error);
options.onError?.(err as Error);
} finally {
setLoading(false);
}
}, [service, options.formatResult, options.onSuccess, options.onError]);
const refresh = useCallback(() => {
return run(...paramsRef.current);
}, [run]);
useEffect(() => {
if (!options.manual) {
run(...(options.defaultParams || []));
}
}, [run, options.manual, options.defaultParams]);
return {
data,
error,
loading,
run,
refresh,
mutate: setData, // 直接修改data
};
};
// 使用示例
const UserInfo = ({ userId }) => {
const { data: user, loading, error } = useRequest(
() => fetch(`/api/users/${userId}`).then(res => res.json()),
{
formatResult: (res) => ({
...res,
fullName: `${res.firstName} ${res.lastName}`,
}),
onError: (err) => {
console.error('获取用户信息失败:', err);
showToast('加载失败,请重试');
}
}
);
if (loading) return <Spinner />;
if (error) return <ErrorDisplay />;
return (
<div>
<h1>{user?.fullName}</h1>
{/* 其他用户信息 */}
</div>
);
};
核心功能:
自动/手动触发模式
结果格式化
生命周期钩子
数据突变能力
请求参数记忆
特性 | 自制useRequest | SWR | React Query | ahooks |
---|---|---|---|---|
自动缓存 | ❌ | ✅ | ✅ | ✅ |
请求去重 | ❌ | ✅ | ✅ | ✅ |
分页支持 | ❌ | ✅ | ✅ | ✅ |
乐观更新 | ❌ | ✅ | ✅ | ✅ |
依赖请求 | ❌ | ✅ | ✅ | ✅ |
轻量级 | ✅ | ✅ | ❌ | ✅ |
学习曲线 | 低 | 中 | 高 | 中 |
选型建议:
简单项目:自制Hook或ahooks
数据密集型:React Query
需要轻量方案:SWR
const useStateMachine = <T extends string>(
initialState: T,
transitions: Record<T, Array<T>> // 状态转移规则
) => {
const [state, setState] = useState(initialState);
const transition = useCallback((newState: T) => {
if (transitions[state].includes(newState)) {
setState(newState);
} else {
console.warn(`Invalid transition from ${state} to ${newState}`);
}
}, [state]);
return [state, transition] as const;
};
// 使用示例
const TrafficLight = () => {
const [light, changeLight] = useStateMachine('red', {
red: ['green'],
green: ['yellow'],
yellow: ['red'],
// 红灯不能直接变黄灯
});
return (
<div>
<div style={{ color: light }}>当前: {light}</div>
<button onClick={() => changeLight('green')}>变绿灯</button>
{/* 其他按钮 */}
</div>
);
};
const useDebounce = <T extends any[]>(
callback: (...args: T) => void,
delay: number
) => {
const timerRef = useRef<NodeJS.Timeout>();
useEffect(() => {
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, []);
return useCallback((...args: T) => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(() => {
callback(...args);
}, delay);
}, [callback, delay]);
};
// 使用示例
const SearchBox = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleSearch = useDebounce(async (value) => {
const res = await fetch(`/api/search?q=${value}`);
setResults(await res.json());
}, 500);
return (
<div>
<input
value={query}
onChange={(e) => {
setQuery(e.target.value);
handleSearch(e.target.value);
}}
/>
{/* 显示结果 */}
</div>
);
};
import { renderHook, act } from '@testing-library/react-hooks';
import { useCounter } from './useCounter';
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
使用useDebugValue展示Hook内部状态
const useToggle = (initialState = false) => {
const [state, setState] = useState(initialState);
useDebugValue(state ? 'On' : 'Off');
// ...
};
React DevTools查看Hook依赖关系
ahooks (阿里出品)
useRequest: 强大的异步管理
useAntdTable: 与Ant Design深度集成
useDrag/useDrop: 拖拽功能
react-use (社区流行)
usePrevious: 获取之前的值
useClickAway: 点击外部区域触发
useLocalStorage: 本地存储同步
Redux Toolkit Hooks
useSelector/useDispatch: Redux集成
useStore: 访问整个store
挑战1:Hook执行顺序必须一致
解决方案:不要在条件/循环中使用Hook
错误示例:
if (condition) {
useEffect(() => {...}); // ❌
}
挑战2:闭包陷阱
解决方案:使用ref保存最新值或使用函数式更新
const [count, setCount] = useState(0);
// 错误方式
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1); // 闭包问题
}, 1000);
return () => clearInterval(timer);
}, []);
// 正确方式
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1); // 函数式更新
}, 1000);
return () => clearInterval(timer);
}, []);
挑战3:性能优化
解决方案:合理使用useMemo/useCallback
const expensiveValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
const stableCallback = useCallback(() => doSomething(a, b), [a, b]);
const useSwitch = (initialValue = false) => {
const [state, setState] = useState(initialValue);
const toggle = useCallback(() => {
setState(prev => !prev);
}, []);
return [state, toggle] as const;
};
// 使用示例
const ToggleButton = () => {
const [on, toggle] = useSwitch(false);
return (
<button onClick={toggle}>
{on ? 'ON' : 'OFF'}
</button>
);
};
通过本文的学习,你应该已经掌握了自定义Hooks从设计到实现的完整知识体系。记住,好的Hook应该像乐高积木一样,既独立完整又能与其他部分完美配合。在实际项目中,不断提炼和优化你的Hooks,它们将成为你React开发中的强大武器。
近日,据 MIT Technology Review 报道,一位名为“Repairnator”的机器人在 GitHub 上“卧底”数月,查找错误并编写和提交修复补丁,结果有多个补丁成功通过并被采纳,这位 Repairnator 到底是如何拯救程序员于水火的呢?
你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗?你还在为搞不清使用哪个生命周期钩子函数而日夜难眠吗?你在还在为组件中的this指向而晕头转向吗?这样看来,说React Hooks是今年最劲爆的新特性真的毫不夸张。
我们将userReducer函数返回的原始dispath命名为origin_dispatch,自定义dispatch函数,当action为函数的时候,我们执行action函数,并将origin_dispatch当作参数传进去;action不是函数,直接调用origin_dispatch,不做处理
使用useEffect 就像瑞士军刀。它可以用于很多事情,从设置订阅到创建和清理计时器,再到更改ref的值。与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。
从 React Hooks 正式发布到现在,我一直在项目使用它。但是,在使用 Hooks 的过程中,我也进入了一些误区,导致写出来的代码隐藏 bug 并且难以维护。这篇文章中,我会具体分析这些问题,并总结一些好的实践,以供大家参考
Hooks 的 API 可以参照 React 官网。本文主要是结合 Demo 详细讲解如何用 Hooks 来实现 React Class Component 写法,让大家更深的理解 Hooks 的机制并且更快的入门。 注意:Rax 的写法和 React 是一致的
以下是上一代标准写法类组件的缺点,也正是hook要解决的问题,型组件很难拆分和重构,也很难测试。业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
Hooks出来已经有段时间了,相信大家都用过段时间了,有没有小伙伴们遇到坑呢,我这边就有个 setInterval 的坑,和小伙伴们分享下解决方案。写个 count 每秒自增的定时器,如下写法结果,界面上 count 为 1 ?
对于 React 16.7 中新的 hooks 系统在社区中引起的骚动,我们都有所耳闻了。人们纷纷动手尝试,并为之兴奋不已。一想到 hooks 时它们似乎是某种魔法,React 以某种甚至不用暴露其实例
9月份开始,使用了React16.8的新特性React Hooks对项目进行了重构,果然,感觉没有被辜负,就像阮一峰老师所说的一样,这个 API 是 React 的未来。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!