一、Hooks 规则(必须遵守)
Hooks(useState/useEffect/useRef…)是 React 16.8 引入的函数,让函数组件拥有状态与副作用,替代类组件。三条铁律:
| 规则 | 说明 |
|---|
| 只在顶层调用 | 不能在条件、循环、嵌套函数中调用(每次渲染顺序必须一致) |
| 只在函数组件/自定义 Hook 中调用 | 不能在普通函数或类组件中调用 |
| 命名以 use 开头 | 自定义 Hook 命名规范 |
if (isLogin) {
useState(0);
}
const [count, setCount] = useState(0);
if (isLogin) { }
二、useState:状态管理
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
const addTwice = () => {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
};
return (
<div>
<p>{count}</p>
<button onClick={addTwice}>+2</button>
</div>
);
}
要点回顾:useState 返回 [值, 更新函数];状态不可变(创建新值替换);更新触发组件重渲染。
三、useEffect:副作用(生命周期替代者)
3.1 三种用法对应生命周期
import { useEffect, useState } from "react";
function App() {
const [id, setId] = useState(1);
useEffect(() => {
console.log("组件挂载(mounted)");
return () => console.log("组件卸载(beforeUnmount)");
}, []);
useEffect(() => {
fetchData(id);
}, [id]);
useEffect(() => {
console.log("每次渲染都执行");
});
}
3.2 生命周期对照表
| 类组件钩子 | Hooks 实现 |
|---|
| componentDidMount | useEffect(() => {...}, []) |
| componentDidUpdate | useEffect(() => {...}, [依赖]) |
| componentWillUnmount | useEffect 的清理函数 |
| 三者合并 | useEffect(() => { ...; return cleanup; }, [依赖]) |
3.3 经典业务:请求数据 + 定时器
function UserList() {
const [list, setList] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
let ignore = false;
fetch("/api/users")
.then((res) => res.json())
.then((data) => {
if (!ignore) setList(data);
setLoading(false);
});
return () => { ignore = true; };
}, []);
return loading ? <p>加载中…</p> : <ul>{list.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}
useEffect(() => {
const timer = setInterval(() => setTime(new Date().toLocaleTimeString()), 1000);
return () => clearInterval(timer);
}, []);
四、useRef:获取 DOM、保存可变变量
4.1 获取 DOM
import { useRef, useEffect } from "react";
function SearchInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="自动聚焦" />;
}
4.2 保存可变变量(不触发重渲染)
function Timer() {
const timerRef = useRef(null);
const countRef = useRef(0);
const start = () => {
timerRef.current = setInterval(() => {
countRef.current += 1;
}, 1000);
};
const stop = () => clearInterval(timerRef.current);
return <><button onClick={start}>开始</button><button onClick={stop}>停止</button></>;
}
4.3 ref 与 useState 的区别
| 对比项 | useRef | useState |
|---|
| 变化是否触发渲染 | ❌ 不触发 | ✅ 触发 |
| 读取 | .current | 直接值 |
| 适用 | DOM、定时器 id、跨渲染可变值 | 需要驱动视图的数据 |
五、常见业务场景
| 场景 | 方案 |
|---|
| 自动聚焦输入框 | useRef + useEffect |
| 定时器/轮询 | useEffect + setInterval + 清理函数 |
| 请求数据 | useEffect + 空依赖 + 防竞态标志 |
| 防抖搜索 | useEffect + setTimeout 清理 |
| 保存上一次值 | useRef 记录 + useEffect 更新 |
| 组件挂载标识 | useRef(false) 首次渲染判断 |
function Search() {
const [keyword, setKeyword] = useState("");
const [results, setResults] = useState([]);
useEffect(() => {
const timer = setTimeout(() => {
setResults([`${keyword} 的结果 1`, `${keyword} 的结果 2`]);
}, 300);
return () => clearTimeout(timer);
}, [keyword]);
return (
<>
<input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
<ul>{results.map((r, i) => <li key={i}>{r}</li>)}</ul>
</>
);
}
六、Hooks 高频踩坑
| 坑 | 现象 | 解决 |
|---|
| 条件里调用 Hook | "Rendered fewer hooks than expected" 报错 | Hook 提到顶层,条件放内部 |
| 依赖数组漏写 | 不更新(漏依赖)或无限循环(依赖对象每次新引用) | 依赖写全;对象依赖用具体字段或 useMemo 稳定引用 |
| 闭包陷阱 | effect 里读到的 state 是旧值 | 依赖数组加上该值,或函数式更新 |
| 直接改 state 对象 | 不触发渲染 | 展开创建新对象 |
| 定时器/订阅未清理 | 内存泄漏、重复触发 | effect 返回清理函数 |
| 渲染中读取 ref.current | 拿到旧值/不一致 | 只在事件或 effect 中读取 |
useEffect(() => {
const timer = setInterval(() => console.log(count), 1000);
return () => clearInterval(timer);
}, []);
七、总结
- 规则:顶层调用、函数组件内调用、use 开头命名;
- useState:状态 + 不可变更新;
- useEffect:空依赖=挂载/卸载、依赖数组=更新、返回清理函数防泄漏;
- useRef:DOM 引用 + 不触发渲染的可变变量;
- 场景:请求、定时器、防抖、聚焦一套组合拳;
- 踩坑:条件调用、依赖缺失、闭包旧值是三大高频问题。