目标:用最少的心智负担,把 Hooks 用“对”、用“稳”、用“快”。
适合:刚学完 React、正在写项目(如路由 + Redux/RTK + axios + styled-components)的同学。
1. Hooks 是什么?解决了什么问题?
Hooks 是 React 在函数组件里提供的一套能力,让你不用 class 也能:
- 管理状态(state)
- 处理副作用(effect)
- 复用逻辑(custom hook)
- 做性能优化(memo / useMemo / useCallback)
- 读写全局状态(Context / Redux hooks)
一句话:Hooks 把“状态 + 副作用 + 复用”这三件事变得更自然、更可组合。
2. Hooks 的两条铁律
React 官方规则(不遵守会出大问题):
-
只能在函数组件或自定义 Hook 的顶层调用 Hook
- 不能在
if / for / while / switch / try-catch里调用
- 不能在
-
不要在普通函数里调用 Hook
- 只能在
Component()或useXxx()里
- 只能在
原因:React 需要依赖“调用顺序”来正确匹配每一个 Hook 的状态。
3. useState:最常用的状态管理
3.1 基本用法
const [count, setCount] = useState(0);
count是当前状态setCount用来更新状态,更新会触发重新渲染
3.2 更新依赖旧值:用函数式更新更稳
setCount(prev => prev + 1);
适合“连续点击 / 多次更新”场景,避免闭包拿到旧值。
4. useEffect:副作用的核心
4.1 什么是“副作用”?
凡是**不是“根据 props/state 计算 UI”**的行为,都属于副作用,比如:
- 请求接口(axios/fetch)
- 订阅事件(addEventListener)
- 操作 DOM
- 定时器
- localStorage
4.2 useEffect 的执行时机(超重要)
useEffect(() => {
// 副作用逻辑
return () => {
// 清理逻辑
};
}, [deps]);
- 第一次渲染完成后执行一次
- 依赖变化时:先执行上一次 cleanup,再执行新的 effect
- 组件卸载时:执行 cleanup
4.3 依赖数组的三种写法(必会)
- 不写依赖:每次渲染后都执行(很少用,容易出坑)
[]:只在挂载时执行一次(常见:首次请求、订阅监听)[a, b]:a/b 变化时重新执行(常见:随条件重新请求)
4.4 清理函数:防止“内存泄漏”
例:给 window 注册点击监听,组件卸载时必须移除。
useEffect(() => {
const onClick = () => setOpen(false);
window.addEventListener("click", onClick, true);
return () => {
window.removeEventListener("click", onClick, true);
};
}, []);
5. useRef:拿 DOM / 存“不会触发渲染”的值
5.1 获取 DOM
const inputRef = useRef(null);
< input ref={inputRef} />
然后:
inputRef.current?.focus();
5.2 保存“跨渲染持久”的变量(不触发更新)
比如计时器 id:
const timerRef = useRef(null);
timerRef.current = setTimeout(...)
6. useMemo:缓存“计算结果”
适用:某个计算很耗时,或者你需要“稳定的引用”避免子组件重渲染。
const total = useMemo(() => {
return expensiveCalc(list);
}, [list]);
注意:useMemo 是优化工具,不是必用工具。先正确,再优化。
7. useCallback:缓存“函数引用”
适用:把函数传给子组件,配合 memo 减少不必要的刷新。
const onTabClick = useCallback((index) => {
setCurrent(index);
}, []);
如果你不写 useCallback,父组件每次渲染都会创建一个新函数引用,子组件可能会以为 props 变了而重渲染。
8. memo:缓存“组件渲染结果”(避免重复渲染)
export default memo(function RoomItem(props) {
...
});
什么时候有用?
- 子组件很复杂,渲染成本高
- 父组件经常重渲染,但子组件 props 很少变
什么时候没必要?
- 组件很简单
- props 每次都在变(例如传新对象、新函数)
9. useContext:跨层级共享数据
适用:主题、语言、用户信息等。
const ThemeContext = createContext(null);
<ThemeContext.Provider value={theme}>
</ThemeContext.Provider>
子组件:
const theme = useContext(ThemeContext);
如果你的项目已经用了 Redux/RTK,一般:
- 业务数据用 Redux
- 主题/配置用 Context 或 styled-components 的 ThemeProvider
10. 自定义 Hook:把“可复用逻辑”抽出来
10.1 为什么要自定义 Hook?
当你发现多个组件里都有:
- 相同的请求逻辑
- 相同的滚动监听
- 相同的表单逻辑
- 相同的状态组合
就可以抽成 useXxx,让逻辑可复用、可测试、可维护。
10.2 示例:封装一个“点击外部关闭”的 Hook
function useClickOutside(handler) {
useEffect(() => {
const onClick = (e) => handler(e);
window.addEventListener("click", onClick, true);
return () => window.removeEventListener("click", onClick, true);
}, [handler]);
}
用法:
useClickOutside(() => setOpen(false));
11. 常见坑点合集
11.1 “依赖数组”写错导致死循环
在 effect 里 setState,又把那个 state 放进依赖里,可能循环触发。
思路:确认依赖是谁、谁变化需要重新执行。
11.2 闭包旧值问题
useEffect(() => {
setInterval(() => console.log(count), 1000);
}, []);
这里永远打印初始 count。解决:
- 用依赖更新 effect
- 或用 ref 保存最新值
11.3 不要把 Hook 当成语法糖乱用
useMemo/useCallback不是写得越多越好- 优化要有目标:减少明显的重复渲染或重计算