14 KiB
tags, create time
| tags | create time | |||||||
|---|---|---|---|---|---|---|---|---|
|
2026-04-29 22:04 |
核心 Hooks
概述
Hook 是 React 16.8 引入的特性,让函数组件拥有 State、副作用管理、上下文消费等原本只有 Class 组件才具备的能力。
[!question] 思考:在 Hook 出现之前,Class 组件有哪些痛点?
| 痛点 | 表现 | Hook 的解法 |
|---|---|---|
| 状态逻辑复用 | 用 HOC / Render Props 嵌套过深("回调地狱") | 自定义 Hook 直接抽离状态逻辑 |
| 职责分散 | 同一逻辑被拆分到 componentDidMount、componentDidUpdate、componentWillUnmount |
同一 useEffect 内组合关联逻辑 + cleanup |
| this 指向混乱 | 需要手动 bind、箭头函数或类字段属性 | 函数组件没有 this,闭包天然解决 |
| 组件臃肿 | 大组件难以阅读和维护 | 按关注点拆分为多个 Hook |
Hook 的设计哲学可以总结为一句话:把"组件做什么"和"组件什么时候做"解耦。
本文聚焦四个核心 Hook —— useState、useEffect、useContext、useRef,并在此基础上介绍性能优化 Hook 与自定义 Hook 实战。
Hook 执行规则
[!warning] 两条铁律
- 只在最顶层调用 Hook — 不能在循环、条件、嵌套函数中调用
- 只在 React 函数组件或自定义 Hook 中调用 — 不能在其他普通 JS 函数中调用
// ❌ 违反规则:条件调用 Hook
function Component({ loaded }) {
if (loaded) {
const [data, setData] = useState(null); // 崩溃!
}
}
// ✅ 正确:条件逻辑放在 Hook 内部
function Component({ loaded }) {
const [data, setData] = useState(null);
useEffect(() => {
if (!loaded) return;
fetch("/api/data").then(setData);
}, [loaded]);
}
Hook 的执行顺序依赖调用顺序
React 内部用一个数组存储每个组件的 Hook 状态。每次渲染时,按声明顺序依次取出对应 Hook 的状态。这就是为什么必须在顶层、固定顺序调用。
function Counter() {
const [count, setCount] = useState(0); // Hook #0
const theme = useContext(ThemeContext); // Hook #1
const btnRef = useRef<HTMLButtonElement>(); // Hook #2
// 每次渲染都按这个顺序取状态,不能颠倒或删除
}
useState
基础用法
const [state, setState] = useState<T>(initialValue);
当初始值计算开销较大时,使用惰性初始化避免重复计算:
const [items, setItems] = useState(() => loadFromStorage()); // 仅首次渲染执行
闭包陷阱(Closure Trap)
[!warning] 高频踩坑点:React 每次渲染都创建全新的作用域和变量副本
const [count, setCount] = useState(0);
setTimeout(() => setCount(count + 1), 1000); // ❌ count 始终为首次渲染时的快照值
为什么? React 的 setCount 不是异步函数,而是 闭包捕获了渲染时的变量快照 —— 第一次渲染后,handleClick 里的 count 就被锁死在 0 上了。
[!tip] 如何修复? 有两种思路:让 React 自己提供最新状态(updater),或者用 ref 持有一个始终更新的引用。具体方案与对比见 → 05-核心 Hooks/01-闭包陷阱
关键行为
[!tip] 核心认知 setState 是异步合并的 —— 你传给它的不是当前 state,而是基于"即将更新"的旧值计算新值。
| 场景 | 行为 |
|---|---|
| 对象/数组作为 state | 引用比较——必须创建新对象/数组才会触发重渲染 |
| 连续多次 setState | React 18 自动合并为一次 re-render |
| 在非 React 代码中调用 | 使用 startTransition / flushSync 控制 |
不可变更新模式
const [list, setList] = useState([{ id: 1, done: false }]);
// ❌ 直接修改 —— 不会触发重渲染
list[0].done = true;
setList(list);
// ✅ 创建新引用 —— 正确触发更新
setList(prev => prev.map(item => item.id === 1 ? { ...item, done: true } : item));
[!note] 为什么要有不可变性? React 通过浅比较引用来判断是否需要重渲染。如果原地修改了原有对象,引用地址不变,React 认为"没变化"就跳过了 DOM 更新。这正是 React 高效的原因之一。
useEffect
基本模型
useEffect(() => {
// 副作用逻辑
return () => { /* cleanup */ }; // 可选清理函数
}, [dependencies]); // 依赖数组
三种行为模式
// 无依赖数组 → 每次渲染后执行(几乎不推荐)
useEffect(() => { console.log("every render"); });
// 空数组 [] → 仅挂载时执行一次
useEffect(() => {
console.log("mount once");
return () => console.log("unmount");
}, []);
// 有依赖项 → 任一依赖变化时重新执行
useEffect(() => {
const sub = api.subscribe(userId);
return () => sub.unsubscribe();
}, [userId]); // userId 变了 → 先 clean 旧的 → 再执行新的
useEffect 生命周期时序
[!abstract] useEffect 的生命周期可以用下面的时序图理解:
sequenceDiagram
participant R as React
participant E as Effect
participant C as Cleanup
R->>E: 渲染完成,执行 effect
Note over E: side effect 运行<br/>(数据请求、DOM 操作等)
R->>R: 再次渲染...
alt 依赖项变化
R->>C: 先执行上一次 effect 的 cleanup
R->>E: 执行新的 effect
else 依赖项未变化
R->>R: 跳过本次 effect
end
[!question] 思考:cleanup 函数何时执行?
- 组件卸载时
- 下一次 effect 执行前(前提是依赖项发生了变化) 这意味着你可以用同一个 effect 同时处理"订阅 → 取消 → 重新订阅"整个流程。
常见副作用类型
// 数据获取
useEffect(() => {
let cancelled = false;
fetch("/api/data")
.then(res => res.json())
.then(data => { if (!cancelled) setData(data); });
return () => { cancelled = true; };
}, []);
// DOM 事件监听
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
// 定时轮询(用 ref 避免把 fetchData 写入依赖数组导致频繁清理)
useEffect(() => {
const fetchData = () => {/* polling logic */};
let mounted = true;
const timer = setInterval(async () => {
if (!mounted) return;
try {
const res = await fetch("/api/data");
const json = await res.json();
setData(json);
} catch (err) {
setError(err instanceof Error ? err : new Error(String(err)));
}
}, 5000);
return () => { mounted = false; clearInterval(timer); };
}, []);
副作用分类指南
[!summary] 哪些该放 useEffect?哪些不该?
| 应该用 useEffect | 不应该用 useEffect |
|---|---|
| 数据获取、订阅、定时器 | JSX 渲染中的同步逻辑 |
| 操作 DOM(focus、测量) | 阻止表单提交、路由跳转 |
| 第三方库集成 | 事件处理函数体内(直接用 onClick 即可) |
useMemo & useCallback
这两个 Hook 都是记忆化工具,核心目标相同:缓存计算结果,避免不必要的重新计算或子组件重渲染。
useMemo —— 缓存计算结果
const sortedItems = useMemo(() => items.sort(comparator), [items]);
[!important] useMemo 不等于"不用每次都算"
useMemo本身也有开销(缓存对比),它只是给开发者一个"提示 React:结果可以被复用"。只有在计算昂贵或在渲染期间产生高成本时才有意义。
useCallback —— 缓存函数引用
const handleClick = useCallback(
(id: string) => { deleteItem(id); },
[deleteItem] // deleteItem 变化时返回新函数
);
两者关系与选择
quadrantChart
title "useMemo vs useCallback 使用决策"
x-axis "少用" --> "常用"
y-axis "缓存计算" --> "缓存引用"
"useCallback": [0.7, 0.9]
"useMemo": [0.3, 0.9]
"纯计算,传给子组件": [0.7, 0.5]
"纯值,渲染中使用": [0.3, 0.5]
| Hook | 缓存什么 | 典型场景 |
|---|---|---|
useMemo |
计算后的值 | 列表排序、过滤、复杂运算 |
useCallback |
函数引用 | 传给 React.memo 包裹的子组件 |
三者联合:防止过度重渲染
function Parent() {
const [text, setText] = useState("");
// 缓存 handler 引用 —— 避免 Child 因 props.fn 变化而重渲染
const handleSubmit = useCallback(
(e: React.FormEvent) => { alert(text); },
[text]
);
// 缓存 computed 值 —— 避免每次渲染都重新 sort/filter
const filteredUsers = useMemo(
() => users.filter(u => u.name.includes(text)),
[users, text]
);
return <Child data={filteredUsers} onSubmit={handleSubmit} />;
}
[!example] 配合 React.memo 的效果
// Child 仅在 props 引用变化时才重渲染 const Child = React.memo(({ data, onSubmit }) => ( <div>{/* ... */}</div> ));如果没有
useCallback和useMemo,即使data和onSubmit内容没变,每次父组件渲染都会生成新引用,导致 Child 无效重渲染。
useContext
基本概念
Context 解决的是"跨层级传参"问题,无需逐层透传 props(又称 prop drilling)。
const ThemeContext = createContext<"light" | "dark">("light");
function App() {
return (
<ThemeContext.Provider value="dark">
<Header /> {/* 不需要 Header 向 Button 传递 theme */}
<Main />
</ThemeContext.Provider>
);
}
function Button() {
const theme = useContext(ThemeContext); // 任意深度消费
return <button className={theme}>Click</button>;
}
最佳实践:封装自定义 Hook
const ThemeContext = createContext<"light" | "dark">("light");
// ❌ 不要到处写 useContext(ThemeContext)
// ✅ 抽取为语义化 Hook
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within a ThemeProvider");
return ctx;
}
Context 的性能陷阱
[!warning] Provider 的 value 一旦变化,所有消费该 Context 的组件都会重渲染
因为每次
{ value: count }都是一个新对象,浅比较不等。
解决方案:
// 方案1:拆分成多个小 Context
const UserContext = createContext(user);
const ThemeContext = createContext(theme);
// 方案2:用 useReducer 保持 dispatch 引用稳定
function App() {
const [state, dispatch] = useReducer(reducer, initial);
return <MyContext.Provider value={{ state, dispatch }}>
<Children />
</MyContext.Provider>;
}
[!tip] 何时用 Context?
- 适合"全局配置":主题、语言、用户信息
- 不适合高频更新的值(频繁变化会导致大量重渲染)
- 复杂状态管理考虑 Zustand / Jotai / Redux Toolkit
useRef
两个用途
// 用途1:持有可变值,修改不触发重渲染
const timerRef = useRef<number | null>(null);
const prevCountRef = useRef<string>("");
// 用途2:访问 DOM 元素
function FocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => { inputRef.current?.focus(); }, []);
return <input ref={inputRef} />;
}
ref vs state 对比
| 维度 | state | ref |
|---|---|---|
| 修改触发重渲染 | ✅ | ❌ |
| 跨渲染持久化 | ✅ | ✅ |
| 写入方式 | setState(val) |
ref.current = val |
| 典型用途 | UI 驱动数据 | DOM 句柄 / 定时器 / 临时计数器 |
[!note] 本质区别 ref 的
.current是一个普通的 JavaScript 属性,修改它就像改任何对象的属性一样,React 完全不知道。State 是 React 管理的响应式数据,修改后会告诉 React "请重新渲染"。
实用技巧:保存上一次的值
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => { ref.current = value; });
return ref.current;
}
// 用法
function Example({ value }) {
const prev = usePrevious(value);
// prev !== undefined && prev !== value → 值变更了
}
自定义 Hook 实战
自定义 Hook 是 React 中最强大的抽象手段之一 —— 它是一个普通函数,名字以 use 开头,内部可以调用其他 Hook,从而实现状态逻辑的跨组件复用。
示例:自动失焦 Hook
function useAutoFocus(delayMs: number = 300) {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
const timer = setTimeout(() => ref.current?.focus(), delayMs);
return () => clearTimeout(timer);
}, [delayMs]);
return ref; // 暴露给组件绑定到 input
}
// 使用
function LoginForm() {
const usernameRef = useAutoFocus();
return <input ref={usernameRef} placeholder="Username" />;
}
示例:在线状态检测
function useIsOnline() {
const [online, setOnline] = useState(navigator.onLine);
useEffect(() => {
setOnline(navigator.onLine);
const onOnline = () => setOnline(true);
const onOffline = () => setOnline(false);
window.addEventListener("online", onOnline);
window.addEventListener("offline", onOffline);
return () => {
window.removeEventListener("online", onOnline);
window.removeEventListener("offline", onOffline);
};
}, []);
return online;
}
自定义 Hook 设计原则
[!summary] 好的自定义 Hook 应遵循以下原则
- 单一职责 —— 每个 Hook 解决一个问题(如上述两个例子各自独立)
- 命名清晰 ——
useXxx格式,读起来像动作:"监听网络"、"记住上次值"- 返回值明确 —— 返回需要的引用或状态,不要暴露过多内部细节
- 可以组合 ——
useFetch内部可用useEffect+useRef,外层可再用useAutoFocus
Hook 调试技巧
[!tip] React DevTools 浏览器扩展 安装后可以:
- 查看每个组件的 Hook 值和更新次数
- 定位"为什么这个组件在重渲染"
- 记录渲染时间,发现性能瓶颈
关联笔记
- 3. 组件篇/01-组件基础.md
- 3. 组件篇/03-性能优化.md
- config/REACT/react-best-practices.md