第 9 章:Hooks 基础
学习目标
- 理解 Hooks 是什么、解决了什么问题
- 掌握 Hooks 的 4 条使用规则
- 熟悉 React 18 内置的所有常用 Hooks
- 避开 Hooks 的 3 个常见坑
一、Hooks 是什么?
Hooks 是 React 16.8 引入的特性,让你在函数组件里"挂钩"状态和生命周期。以前只有 class 组件能用 state 和生命周期。
// class 写法(旧)
class Counter extends React.Component {
state = { count: 0 };
render() {
return <button onClick={() => this.setState({ count: this.state.count + 1 })}>{this.state.count}</button>;
}
}
// hooks 写法(新)
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}Hooks 优势:
- 没有
this烦恼 - 逻辑复用方便(自定义 Hook)
- 组件更扁平
- 函数组件 + Hooks 是当前主流
二、React 内置 Hooks 一览
| Hook | 作用 |
|---|---|
useState | 组件 state |
useEffect | 副作用(数据获取、订阅、DOM 操作) |
useContext | 跨组件共享数据 |
useReducer | 复杂 state 管理 |
useRef | 保留值/DOM 引用 |
useMemo | 缓存计算结果 |
useCallback | 缓存函数引用 |
useLayoutEffect | 同步版 useEffect |
useImperativeHandle | 自定义 ref 暴露的方法 |
useDebugValue | DevTools 显示调试信息 |
useId | 生成稳定唯一 ID |
useTransition | 标记非紧急更新(并发) |
useDeferredValue | 延迟更新值 |
useSyncExternalStore | 订阅外部 store |
三、Hooks 的 4 条铁律
- 只在顶层调用(不要在 if/for/函数里调用)
- 只在 React 函数里调用(组件 / 自定义 Hook)
- 命名以
use开头(自定义 Hook) - 依赖数组要写全(配合 ESLint 检查)
// ❌ 错误:if 里调用 hook
function Bad({ cond }: { cond: boolean }) {
if (cond) {
const [count, setCount] = useState(0); // ⚠️ 报错!
}
}
// ✅ 正确:无条件调用
function Good({ cond }: { cond: boolean }) {
const [count, setCount] = useState(0); // 永远执行
if (!cond) return null;
return <div>{count}</div>;
}⚠️ 坑 1:条件调用 hook 会导致 hooks 数量不稳定,React 内部按调用顺序存储 state,数量变了 state 全乱套。
四、为什么要顶层调用?
React 内部用链表按调用顺序存 state:
function Comp() {
const [a, setA] = useState('A'); // 第 1 个 hook
const [b, setB] = useState('B'); // 第 2 个 hook
return ...;
}如果某次渲染跳过了 useState('A'),那 setB 实际拿到的是 A 的 setter,数据全错乱。
五、为什么只能在 React 函数里?
Hooks 依赖 React 的内部机制(当前的 Fiber 节点、调用栈),普通 JS 函数调用 hook 会找不到上下文而报错。
// ❌ 普通工具函数里不能 hook
function myUtil() {
const [x, setX] = useState(0); // ⚠️ 报错!
}
// ✅ 自定义 Hook 里调用是合法的
function useMyState(initial: number) {
const [x, setX] = useState(initial); // OK
return [x, setX];
}⚠️ 坑 2:ESLint 配置要开
react-hooks/rules-of-hooks,强制顶层/函数组件调用。
六、自定义 Hook 是什么?
以 use 开头的函数,内部调用其他 Hook,封装可复用逻辑:
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : initial;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
// 使用
function Comp() {
const [name, setName] = useLocalStorage('name', '游客');
return <input value={name} onChange={e => setName(e.target.value)} />;
}⚠️ 坑 3:自定义 Hook 本质是普通函数,React 不识别它,靠约定(命名 + 调用其他 Hook)让它"成为"Hook。
七、React 18 新 Hooks 速览
useTransition
React 18 的"优先级调度"Hook——把更新拆成紧急和不紧急,不紧急的可以让步给紧急的(用户输入不卡)。
核心问题:大列表搜索卡顿
function Search() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
// 1 万条数据 filter,可能要 200ms
function handleChange(e) {
setQuery(e.target.value)
setResults(filterList(e.target.value)) // ← 这一步卡 200ms
}
}后果:输入框要等 200ms 才更新,用户以为"没打上",越打越多。
用 useTransition 解决:
const [isPending, startTransition] = useTransition()
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value) // 紧急:输入框立即更新
startTransition(() => {
setResults(filterList(e.target.value)) // 非紧急:可中断、可延迟
})
}结果:
- 输入框立即响应(紧急优先)
- 列表慢慢算,不算完也不卡 UI
isPending === true期间可以显示 loading
| 概念 | 含义 |
|---|---|
setQuery | 紧急更新——必须立即反映 |
startTransition | 非紧急更新——可中断、可延迟、可让步 |
isPending | 非紧急更新还没算完就是 true |
实战用法:加 loading 提示
function Search() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
function handleChange(e) {
setQuery(e.target.value)
startTransition(() => {
setResults(filterList(e.target.value))
})
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<List items={results} />
</div>
)
}useTransition vs useDeferredValue
// useTransition:包更新动作(主动控制)
startTransition(() => {
setResults(filterList(e.target.value))
})
// useDeferredValue:包值(值延迟一下)
const deferredQuery = useDeferredValue(query)
const results = filterList(deferredQuery)| useTransition | useDeferredValue | |
|---|---|---|
| 包啥 | 更新动作(setX) | 值(const d = useDeferredValue(v)) |
| 主动权 | 在你 | 在 React 自动延迟 |
| 适用场景 | 搜索/过滤大列表、tab 切换 | input 实时派生数据 |
啥时候用
| 场景 | 用啥 |
|---|---|
| 搜索/过滤大列表 | useTransition |
| 切换 tab 渲染重组件 | useTransition |
| 实时搜索(input + 派生值) | useDeferredValue |
| 普通小数据更新 | 不用,直接 setState |
💡
useTransition= "紧急/不紧急"开关——setX直接更新 = 紧急;startTransition(() => setX(...))= 不紧急,可以让步。大列表/重渲染才用,小数据别瞎包。
useDeferredValue
const [query, setQuery] = useState('');
const deferred = useDeferredValue(query); // 延迟一帧更新
// 用 deferred 做重型计算,输入仍流畅
const filtered = bigList.filter(x => x.includes(deferred));为啥 filter 不用 query?——query 同步跑 filter,输入框跟着阻塞;deferred 让 filter 慢一拍,输入框立即响应。
query → 输入框(必须用最新的,不能延迟)
deferred → 重计算(用旧值,让 filter 慢慢跑)为啥 deferred 能用"旧值"?——中间会短暂"旧结果",但只持续一帧(16ms 左右),用户基本看不出来:
| 时刻 | query | deferred | 列表内容 |
|---|---|---|---|
| 0 ms | "" | "" | filter("") = [] ✅ |
| 输入 "a" | "a" | "" | filter("") = [] ⚠️ 短暂旧 |
| 66 ms | "a" | "a" | filter("a") = [...] ✅ 跟上 |
取舍:输入永远流畅 > 列表短暂旧 1 帧——用户眼睛盯着输入框,没盯着列表,延迟 1 帧无感。
啥时候不能用 deferred(数据必须实时准):
// ❌ 金额/状态/计数 → 必须用 query,延迟会算错钱
const total = orders.filter(o => o.includes(query)).reduce((s, o) => s + o.amount, 0)
// ✅ 搜索/过滤/排序 → 可以用 deferred,旧 1 帧无所谓
const filtered = bigList.filter(x => x.includes(deferred))判断:用 query !== deferred 判断"deferred 是否落后",显示 loading:
const isStale = query !== deferred
{isStale && <Spinner />}💡 deferred 短暂旧 1 帧,换取输入永远流畅——搜索/过滤/排序随便用;金额/状态/计数别用。
useId
const id = useId();
<label htmlFor={id}>姓名</label>
<input id={id} />八、本章小结
| 要点 | 关键 |
|---|---|
| Hooks 价值 | 函数组件也能用 state/lifecycle |
| 铁律 | 顶层调用 / 函数组件或自定义 Hook |
| 自定义 Hook | use 开头,封装可复用逻辑 |
| React 18 新 | useTransition / useDeferredValue / useId |
| 顺序敏感 | 内部用顺序存 state,不能条件调用 |
| 配套工具 | ESLint react-hooks 插件 |
动手练习
- 阅读列表:列出你能记得的所有内置 Hook,作用一句话
- 坏代码改造:把 if 里调 hook 的代码改成正确写法
- useId:用
useId给表单字段配 label - useTransition:写一个搜索框,输入时用
useTransition包过滤逻辑
下一章:第 10 章:useState →