vault backup: 2026-04-29 22:19:51
This commit is contained in:
@@ -0,0 +1,495 @@
|
||||
---
|
||||
tags: [React, Hooks, useState, useEffect, useContext, useRef, Frontend]
|
||||
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] 两条铁律
|
||||
> 1. **只在最顶层调用 Hook** — 不能在循环、条件、嵌套函数中调用
|
||||
> 2. **只在 React 函数组件或自定义 Hook 中调用** — 不能在其他普通 JS 函数中调用
|
||||
|
||||
```tsx
|
||||
// ❌ 违反规则:条件调用 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 的状态**。这就是为什么必须在顶层、固定顺序调用。
|
||||
|
||||
```tsx
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0); // Hook #0
|
||||
const theme = useContext(ThemeContext); // Hook #1
|
||||
const btnRef = useRef<HTMLButtonElement>(); // Hook #2
|
||||
|
||||
// 每次渲染都按这个顺序取状态,不能颠倒或删除
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## useState
|
||||
|
||||
### 基础用法
|
||||
|
||||
```tsx
|
||||
const [state, setState] = useState<T>(initialValue);
|
||||
```
|
||||
|
||||
当初始值计算开销较大时,使用惰性初始化避免重复计算:
|
||||
|
||||
```tsx
|
||||
const [items, setItems] = useState(() => loadFromStorage()); // 仅首次渲染执行
|
||||
```
|
||||
|
||||
### 关键行为
|
||||
|
||||
> [!tip] 核心认知
|
||||
> setState 是**异步合并**的 —— 你传给它的不是当前 state,而是基于"即将更新"的旧值计算新值。
|
||||
|
||||
| 场景 | 行为 |
|
||||
|------|------|
|
||||
| 对象/数组作为 state | **引用比较**——必须创建新对象/数组才会触发重渲染 |
|
||||
| 连续多次 setState | React 18 自动合并为一次 re-render |
|
||||
| 在非 React 代码中调用 | 使用 `startTransition` / `flushSync` 控制 |
|
||||
|
||||
```tsx
|
||||
function BadCounter() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
// ❌ 闭包陷阱:count 始终捕获定义时的 0
|
||||
setTimeout(() => setCount(count + 1), 1000);
|
||||
}
|
||||
|
||||
function GoodCounter() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
// ✅ 使用 updater 函数,获取最新值
|
||||
setTimeout(() => setCount(c => c + 1), 1000);
|
||||
}
|
||||
```
|
||||
|
||||
### 不可变更新模式
|
||||
|
||||
```tsx
|
||||
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
|
||||
|
||||
### 基本模型
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
// 副作用逻辑
|
||||
return () => { /* cleanup */ }; // 可选清理函数
|
||||
}, [dependencies]); // 依赖数组
|
||||
```
|
||||
|
||||
### 三种行为模式
|
||||
|
||||
```tsx
|
||||
// 无依赖数组 → 每次渲染后执行(几乎不推荐)
|
||||
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 的生命周期可以用下面的时序图理解:
|
||||
|
||||
```mermaid
|
||||
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 同时处理"订阅 → 取消 → 重新订阅"整个流程。
|
||||
|
||||
### 常见副作用类型
|
||||
|
||||
```tsx
|
||||
// 数据获取
|
||||
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 —— 缓存计算结果
|
||||
|
||||
```tsx
|
||||
const sortedItems = useMemo(() => items.sort(comparator), [items]);
|
||||
```
|
||||
|
||||
> [!important] useMemo 不等于"不用每次都算"
|
||||
> `useMemo` 本身也有开销(缓存对比),它只是给开发者一个"提示 React:结果可以被复用"。**只有在计算昂贵或在渲染期间产生高成本时才有意义。**
|
||||
|
||||
### useCallback —— 缓存函数引用
|
||||
|
||||
```tsx
|
||||
const handleClick = useCallback(
|
||||
(id: string) => { deleteItem(id); },
|
||||
[deleteItem] // deleteItem 变化时返回新函数
|
||||
);
|
||||
```
|
||||
|
||||
### 两者关系与选择
|
||||
|
||||
```mermaid
|
||||
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` 包裹的子组件 |
|
||||
|
||||
### 三者联合:防止过度重渲染
|
||||
|
||||
```tsx
|
||||
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 的效果
|
||||
> ```tsx
|
||||
> // Child 仅在 props 引用变化时才重渲染
|
||||
> const Child = React.memo(({ data, onSubmit }) => (
|
||||
> <div>{/* ... */}</div>
|
||||
> ));
|
||||
> ```
|
||||
> 如果没有 `useCallback` 和 `useMemo`,即使 `data` 和 `onSubmit` 内容没变,每次父组件渲染都会生成新引用,导致 Child 无效重渲染。
|
||||
|
||||
---
|
||||
|
||||
## useContext
|
||||
|
||||
### 基本概念
|
||||
|
||||
Context 解决的是"跨层级传参"问题,无需逐层透传 props(又称 prop drilling)。
|
||||
|
||||
```tsx
|
||||
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
|
||||
|
||||
```tsx
|
||||
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 }` 都是一个新对象,浅比较不等。
|
||||
|
||||
**解决方案:**
|
||||
|
||||
```tsx
|
||||
// 方案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
|
||||
|
||||
### 两个用途
|
||||
|
||||
```tsx
|
||||
// 用途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 "请重新渲染"。
|
||||
|
||||
### 实用技巧:保存上一次的值
|
||||
|
||||
```tsx
|
||||
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
|
||||
|
||||
```tsx
|
||||
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" />;
|
||||
}
|
||||
```
|
||||
|
||||
### 示例:在线状态检测
|
||||
|
||||
```tsx
|
||||
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 应遵循以下原则
|
||||
> 1. **单一职责** —— 每个 Hook 解决一个问题(如上述两个例子各自独立)
|
||||
> 2. **命名清晰** —— `useXxx` 格式,读起来像动作:"监听网络"、"记住上次值"
|
||||
> 3. **返回值明确** —— 返回需要的引用或状态,不要暴露过多内部细节
|
||||
> 4. **可以组合** —— `useFetch` 内部可用 `useEffect` + `useRef`,外层可再用 `useAutoFocus`
|
||||
|
||||
---
|
||||
|
||||
## Hook 调试技巧
|
||||
|
||||
> [!tip] React DevTools 浏览器扩展
|
||||
> 安装后可以:
|
||||
> - 查看每个组件的 Hook 值和更新次数
|
||||
> - 定位"为什么这个组件在重渲染"
|
||||
> - 记录渲染时间,发现性能瓶颈
|
||||
|
||||
---
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[3. 组件篇/01-组件基础.md]]
|
||||
- [[3. 组件篇/03-性能优化.md]]
|
||||
- [[config/REACT/react-best-practices.md]]
|
||||
@@ -0,0 +1,421 @@
|
||||
---
|
||||
tags: [React, Hooks, Performance, useMemo, useCallback, Frontend]
|
||||
create time: 2026-04-29 22:05
|
||||
---
|
||||
|
||||
# 性能优化 Hooks
|
||||
|
||||
## 概述
|
||||
|
||||
当 React 应用出现不必要的重渲染时,开发者最先想到的是 `useMemo` 和 `useCallback`。但它们也有认知成本——**不恰当的使用反而会让代码更慢更乱**。理解何时该用、何时不该用是关键。
|
||||
|
||||
> [!question] 思考:为什么大部分 React 应用根本不需要 useMemo?
|
||||
> 现代浏览器处理一个普通 JavaScript 函数只需几微秒,而 `useMemo` 自身就有缓存对比 + 闭包引用的开销。在没有测量(measure)之前就加记忆化,本质上是猜。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["发现卡顿"] --> B{"是否在 DevTools<br/>Profiler 中定位?"}
|
||||
B -->|否| C["先安装 React DevTools<br/>定位瓶颈组件"]
|
||||
B -->|是| D{"子组件是否因<br/>props 引用变化重渲染?"}
|
||||
D -->|是| E["给子组件加 React.memo"]
|
||||
D -->|否| F{"计算是否真的昂贵?"}
|
||||
F -->|是| G["用 useMemo 缓存结果"]
|
||||
F -->|否| H["不要优化 — 保持简单"]
|
||||
E --> I{"回调引用是否<br/>每次都被替换?"}
|
||||
I -->|是| J["用 useCallback 稳定引用"]
|
||||
I -->|否| K["不要优化"]
|
||||
|
||||
style C fill:#F5A87D,color:#000
|
||||
style H fill:#F5A87D,color:#000
|
||||
style K fill:#F5A87D,color:#000
|
||||
style G fill:#4FC08D,color:#fff
|
||||
style J fill:#4FC08D,color:#fff
|
||||
```
|
||||
|
||||
本文聚焦以下 Hooks,按实用场景排序:
|
||||
|
||||
| Hook | 作用 | React 版本 |
|
||||
|------|------|------------|
|
||||
| `useMemo` | 缓存计算结果 | 16.8+ |
|
||||
| `useCallback` | 缓存函数引用 | 16.8+ |
|
||||
| `useTransition` | 标记低优先级更新 | 18+ |
|
||||
| `useDeferredValue` | 产生延迟值副本 | 18+ |
|
||||
| `useDebugValue` | 自定义 Hook 调试标记 | 16.8+ |
|
||||
| `useId` | 服务端一致的唯一 ID | 18.3+ |
|
||||
|
||||
> [!note] 前置知识
|
||||
> 这些 Hooks 建立在 `useState` / `useEffect` 的基础上。如果你还不熟悉核心 Hooks,请先阅读 [[05-核心 Hooks]]。
|
||||
|
||||
---
|
||||
|
||||
## useMemo
|
||||
|
||||
```tsx
|
||||
const memoizedValue = useMemo(() => computeExpensive(a, b), [a, b]);
|
||||
```
|
||||
|
||||
### 执行时机
|
||||
|
||||
| 时机 | 行为 |
|
||||
|------|------|
|
||||
| 依赖未变 | 返回上次缓存的值(不调用计算函数) |
|
||||
| 任一依赖变化 | 重新执行计算函数并缓存新结果 |
|
||||
| 组件首次渲染 | 执行计算(无缓存可用) |
|
||||
|
||||
### 适用场景
|
||||
|
||||
```tsx
|
||||
// ✅ 适用:昂贵计算 + 频繁 re-render
|
||||
const sortedUsers = useMemo(() => {
|
||||
return users.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, [users]);
|
||||
|
||||
// ✅ 适用:复杂对象/数组衍生值
|
||||
const userStats = useMemo(() => ({
|
||||
total: users.length,
|
||||
active: users.filter(u => u.active).length,
|
||||
avgAge: users.reduce((sum, u) => sum + u.age, 0) / users.length,
|
||||
}), [users]);
|
||||
|
||||
// ❌ 不适用:简单运算(开销比 useMemo 本身还大)
|
||||
const double = useMemo(() => count * 2, [count]);
|
||||
```
|
||||
|
||||
> [!tip] 判断标准
|
||||
> - 如果表达式只包含 `+ - * /` 或简单的 `.filter()` / `.map()` → 不需要 useMemo
|
||||
> - 如果涉及 API 调用、深层遍历、DOM 测量、大量排序 → 考虑 useMemo
|
||||
> - 如果传给了 `React.memo` 包裹的子组件作为 prop → 优先考虑 useMemo
|
||||
|
||||
### 常见陷阱
|
||||
|
||||
> [!warning] 依赖陷阱:useMemo 的第二个参数决定了一切
|
||||
> ```tsx
|
||||
> // ❌ 忘记放入依赖 —— 闭包捕获旧值
|
||||
> const memo = useMemo(() => items.filter(i => i.price > minPrice), [items]);
|
||||
> // minPrice 变了但 memo 没刷新!
|
||||
>
|
||||
> // ✅ 列出所有外部引用
|
||||
> const memo = useMemo(() => items.filter(i => i.price > minPrice), [items, minPrice]);
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
## useCallback
|
||||
|
||||
```tsx
|
||||
const memoizedCallback = useCallback(
|
||||
(arg1: string, arg2: number) => doSomething(arg1, arg2),
|
||||
[dep1, dep2],
|
||||
);
|
||||
```
|
||||
|
||||
### 本质
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["每次渲染创建新函数"] --> B["子组件收到新引用"]
|
||||
B --> C["子组件 re-render<br/>即使 props 内容没变"]
|
||||
|
||||
D["useCallback 包装"] --> E["保持同一函数引用"]
|
||||
E --> F["memo / React.memo<br/>拦截重渲染"]
|
||||
|
||||
style C fill:#F5A87D,color:#000
|
||||
style F fill:#4FC08D,color:#fff
|
||||
```
|
||||
|
||||
### 典型使用模式
|
||||
|
||||
```tsx
|
||||
// 父组件:稳定 callback 引用传给子组件
|
||||
function Parent() {
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
// 不包装:每次 render 都是新函数 → Child 永远 re-render
|
||||
// const handleChange = (e) => setQuery(e.target.value);
|
||||
|
||||
// 包装:引用稳定,Child 在 query 不变时不重渲染
|
||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setQuery(e.target.value);
|
||||
}, []); // setQuery 引用稳定(setState),可不列入依赖
|
||||
|
||||
return <SearchInput value={query} onChange={handleChange} />;
|
||||
}
|
||||
|
||||
// 子组件:配合 React.memo 生效
|
||||
const SearchInput = React.memo(({ value, onChange }: { value: string; onChange: (e: React.ChangeEvent<HTMLInputElement>) => void }) => {
|
||||
return <input value={value} onChange={onChange} />;
|
||||
});
|
||||
```
|
||||
|
||||
> [!example] useCallback + React.memo 联合效果
|
||||
> 见 [[05-核心 Hooks]] 中"**三者联合:防止过度重渲染**"章节,有完整示例。
|
||||
|
||||
### 依赖数组设计原则
|
||||
|
||||
> [!summary] 不要把所有局部变量都塞进依赖数组
|
||||
> ```tsx
|
||||
> // ❌ 错误思路:把所有变量都列上
|
||||
> const handler = useCallback(() => {
|
||||
> doSomething(a, b, c, d, e, f);
|
||||
> }, [a, b, c, d, e, f]);
|
||||
> // 几乎所有变量都会变 → 几乎每次都生成新函数 → 失去意义
|
||||
>
|
||||
> // ✅ 正确思路:思考"哪些真正影响这个回调的行为"
|
||||
> const handler = useCallback(() => {
|
||||
> fetchData(keyword); // keyword 是唯一外部依赖
|
||||
> }, [keyword]);
|
||||
> ```
|
||||
>
|
||||
> **关键经验**:`setState` setter 函数和 `useRef.current` 引用是稳定的,不需要放入依赖数组。
|
||||
|
||||
---
|
||||
|
||||
## useTransition(React 18)
|
||||
|
||||
```tsx
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
function SearchPage() {
|
||||
const [query, setQuery] = useState("");
|
||||
const [results, setResults] = useState([]);
|
||||
|
||||
const handleChange = (q: string) => {
|
||||
setQuery(q); // 同步更新(立即渲染,高优先级)
|
||||
|
||||
// 标记为 transition:低优先级更新
|
||||
startTransition(() => {
|
||||
setResults(performSearch(q)); // 延迟渲染,不阻塞 UI
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input value={query} onChange={e => handleChange(e.target.value)} />
|
||||
{isPending && <Spinner />}
|
||||
<Results list={results} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 语义解读
|
||||
|
||||
`useTransition` 的核心思想是:**将一批状态更新分为"马上显示"和"稍后显示"**。`startTransition` 内部的 setState 会被降级为后台优先级,React 可以随时中断它去响应用户交互。
|
||||
|
||||
### 使用场景区别
|
||||
|
||||
```mermaid
|
||||
quadrantChart
|
||||
title "更新优先级划分"
|
||||
x-axis "高优先级" --> "低优先级"
|
||||
y-axis "数据驱动" --> "UI 展示"
|
||||
"onClick → setState": [0.8, 0.9]
|
||||
"路由切换": [0.7, 0.7]
|
||||
"search → results": [0.3, 0.8]
|
||||
"scroll animation": [0.2, 0.3]
|
||||
"typing in input": [0.9, 0.5]
|
||||
```
|
||||
|
||||
### useTransition vs useDeferredValue
|
||||
|
||||
| 维度 | useTransition | useDeferredValue |
|
||||
|------|---------------|-------------------|
|
||||
| 控制粒度 | 包裹特定 setState | 包裹整个值 |
|
||||
| 语义 | 启动一个低优先级事务 | 产生一个延迟副本 |
|
||||
| 适合场景 | 表单提交后加载详情 | 搜索框前后分屏 |
|
||||
| 使用复杂度 | 需手动选择哪些更新降级 | 一行封装,自动推导 |
|
||||
|
||||
> [!tip] 如何选择?
|
||||
> - 你想让 UI 看起来流畅(输入立刻响应,结果稍后出来)→ `useDeferredValue`
|
||||
> - 你有一组相关更新想让它们一起降级 → `useTransition`
|
||||
> - 不确定?先用 `useTransition`,它在大多数场景下都能工作
|
||||
|
||||
---
|
||||
|
||||
## useDeferredValue(React 18)
|
||||
|
||||
```tsx
|
||||
function SearchPage() {
|
||||
const [query, setQuery] = useState("");
|
||||
const deferredQuery = useDeferredValue(query); // query 即时更新,deferredQuery 延迟更新
|
||||
|
||||
return (
|
||||
<>
|
||||
<input value={query} onChange={e => setQuery(e.target.value)} />
|
||||
{/* 快速响应用户输入 */}
|
||||
<QuickSuggestions keyword={query} />
|
||||
{/* 耗时操作延迟渲染 */}
|
||||
<HeavyResultsList keyword={deferredQuery} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 内部工作原理
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 用户输入
|
||||
participant S as useState (query)
|
||||
participant D as useDeferredValue
|
||||
participant R as React Scheduler
|
||||
|
||||
U->>S: setQuery("abc")
|
||||
S-->>R: 高优先级更新(立即渲染)
|
||||
Note over R: 输入框立刻显示 "abc"
|
||||
R->>D: query 已更新
|
||||
D->>R: 请求延迟值(可被抢占)
|
||||
Note over R: 如果有更高优先级事件<br/>会暂停 deferred 渲染
|
||||
R->>R: 空闲时渲染 HeavyResultsList
|
||||
```
|
||||
|
||||
> [!question] 思考:deferred 值延迟多久?
|
||||
> React 没有固定的延迟时间。它的策略是:"如果有更高优先级的工作要处理(比如用户正在打字),就暂缓 deferred 渲染;等浏览器空闲了再补上。"这使得它既能保证流畅性,又能最终呈现结果。
|
||||
|
||||
---
|
||||
|
||||
## useDebugValue(自定义 Hook 调试)
|
||||
|
||||
> [!abstract] 当你编写自定义 Hook 时,如何在 React DevTools 中看到有意义的值?
|
||||
> 这就是 `useDebugValue` 的用武之地 —— 它为自定义 Hook 添加自定义显示标签。
|
||||
|
||||
```tsx
|
||||
function useOnlineStatus() {
|
||||
const [online, setOnline] = useState(navigator.onLine);
|
||||
|
||||
// 👇 在 DevTools 中显示可读的 "🟢 Online" 或 "🔴 Offline"
|
||||
useDebugValue(online ? "🟢 Online" : "🔴 Offline");
|
||||
|
||||
useEffect(() => {
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
### 延迟格式化(高性能场景)
|
||||
|
||||
```tsx
|
||||
function useFetch(url: string) {
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
// 延迟格式化:只在 DevTools 展开时才执行 format 函数
|
||||
// 避免在开发环境中对大型数据集造成额外开销
|
||||
useDebugValue(data, d =>
|
||||
d ? `${d.items?.length || 0} items loaded` : "loading..."
|
||||
);
|
||||
|
||||
// ... fetch logic
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
> [!tip] 生产环境安全
|
||||
> `useDebugValue` 在生产构建中会被自动忽略,不会产生任何运行时开销。放心在自定义 Hook 中使用。
|
||||
|
||||
---
|
||||
|
||||
## useId(React 18.3+)
|
||||
|
||||
> [!important] SSR 兼容的唯一 ID
|
||||
> `useId` 专为 accessibility 属性设计(如 `aria-labelledby`、`htmlFor`)。它保证了服务端和客户端生成的 ID 完全一致,解决了 hydrate mismatch 问题。
|
||||
|
||||
```tsx
|
||||
function FormField() {
|
||||
// 每次渲染生成唯一且稳定的 ID
|
||||
const labelId = useId();
|
||||
const inputId = useId();
|
||||
|
||||
return (
|
||||
<>
|
||||
<label id={labelId}>Username</label>
|
||||
<input id={inputId} aria-labelledby={labelId} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### useId vs Math.random() vs 手动字符串
|
||||
|
||||
| 方式 | SSR 一致 | Hydrate 匹配 | 确定性 |
|
||||
|------|----------|-------------|--------|
|
||||
| `useId()` | ✅ | ✅ | ✅ 基于树结构 |
|
||||
| `Math.random()` | ❌ | ❌ | ❌ 每次随机 |
|
||||
| 手动 `"field-1"` | ✅ | ⚠️ 需人工维护 | ⚠️ 易重复 |
|
||||
|
||||
> [!warning] useId 不适合用于 key
|
||||
> useId 生成的 ID 是稳定的但不一定是唯一的(同一组件多次渲染可能返回相同 ID)。列表的 `key` 仍应使用业务标识符。
|
||||
|
||||
---
|
||||
|
||||
## 性能调优 Checklist
|
||||
|
||||
> [!summary] 优化顺序
|
||||
> 不要一开始就用 useMemo/useCallback!按以下顺序排查:
|
||||
|
||||
1. **减少不必要的 state** — 能派生的从 state 中移除(derived state)
|
||||
2. **拆分巨型组件** — 子组件独立维护自己的 state
|
||||
3. **React DevTools Profiler** — 定位哪个组件在多余 re-render
|
||||
4. **给高频子组件加 React.memo**
|
||||
5. **最后才考虑 useMemo / useCallback**
|
||||
|
||||
```tsx
|
||||
// Step 1: 先移除所有 useMemo/useCallback 做基准测试
|
||||
// Step 2: 加上 React.memo 包裹频繁更新的子组件
|
||||
// Step 3: 仅针对仍存在的瓶颈点添加 useMemo/useCallback
|
||||
```
|
||||
|
||||
### React DevTools Profiler 实操
|
||||
|
||||
```tsx
|
||||
import { Profiler } from "react";
|
||||
|
||||
function onRenderCallback(
|
||||
id: string, // "MyComponent"
|
||||
phase: "mount" | "update",
|
||||
actualDuration: number // 本次渲染耗时(ms)
|
||||
) {
|
||||
console.log(`${id} ${phase}: ${actualDuration.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
// 用法:包裹需要监控的 subtree
|
||||
<Profiler id="App" onRender={onRenderCallback}>
|
||||
<App />
|
||||
</Profiler>
|
||||
```
|
||||
|
||||
> [!tip] Profiler API vs DevTools 界面
|
||||
> - **Profiler API**:适合持续记录生产环境中的性能数据
|
||||
> - **DevTools 界面**:适合开发阶段交互式点击分析哪个组件导致了重渲染
|
||||
> 两者可以结合使用。
|
||||
|
||||
---
|
||||
|
||||
## 何时绝对不要用这些 Hook?
|
||||
|
||||
> [!question] 思考:哪些场景加了优化反而适得其反?
|
||||
|
||||
| 场景 | 原因 |
|
||||
|------|------|
|
||||
| 简单算术运算 | `count * 2` 的开销远小于闭包比较 |
|
||||
| 低频渲染组件 | 一年才重渲染一次的东西不需要优化 |
|
||||
| 顶层容器组件 | 容器本身很少子组件,优化收益为零 |
|
||||
| 嵌套过深的依赖 | `useCallback(() => fn(a,b,c,d,e), [a,b,c,d,e])` → 等价于不用 |
|
||||
|
||||
> [!note] Google 的前端性能研究结论
|
||||
> 在他们的实际项目度量中,超过 90% 的重渲染问题通过合理的 `React.memo` 就能解决,真正需要 `useMemo` / `useCallback` 的场景不足 5%。
|
||||
|
||||
---
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[05-核心 Hooks]] — useState / useEffect / useContext / useRef 详解
|
||||
- [[07-自定义 Hooks]] — 如何设计和组合可复用的自定义 Hook
|
||||
@@ -0,0 +1,463 @@
|
||||
---
|
||||
tags: [React, Custom Hooks, Frontend]
|
||||
create time: 2026-04-29 22:30
|
||||
---
|
||||
|
||||
# 自定义 Hooks
|
||||
|
||||
## 概述
|
||||
|
||||
自定义 Hook 是 React 中最强大的逻辑复用机制。它以函数形式封装可复用的副作用逻辑和状态,通过 Hook 组合实现"代码即组件"的哲学。理解如何设计良好的自定义 Hook,是从中级迈向高级 React 开发者的分水岭。
|
||||
|
||||
## 命名规范与设计原则
|
||||
|
||||
> [!question] 思考
|
||||
> 为什么普通函数不能直接持有 React state?Hook 和普通函数的根本区别是什么?
|
||||
> (提示:从 React 的内部执行上下文和 Fiber 架构角度理解)
|
||||
|
||||
### 必须以 `use` 开头
|
||||
|
||||
```tsx
|
||||
// ✅ 正确
|
||||
function useFetch(url: string) { ... }
|
||||
function useLocalStorage<T>(key: string) { ... }
|
||||
function useWindowSize() { ... }
|
||||
|
||||
// ❌ 错误:不遵循 use 前缀约定,React 无法识别为 Hook
|
||||
function fetchWithRetry(url: string) { ... }
|
||||
```
|
||||
|
||||
> [!warning] 前置依赖
|
||||
> 所有自定义 Hook 示例默认已导入:
|
||||
> ```tsx
|
||||
> import { useState, useEffect, useCallback, useMemo, useRef, RefObject } from "react";
|
||||
> ```
|
||||
|
||||
### 规则与常规函数不同
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["自定义 Hook"] --> B["可以在任何条件/循环内调用"]
|
||||
A --> C["可以嵌套调用(Hook 中再调其他 Hook)"]
|
||||
A --> D["⚠️ 但必须在组件顶层或 Hook 中调用"]
|
||||
|
||||
D1["不能在回调/普通函数内调用"] --> D
|
||||
|
||||
E["普通函数"] --> F["无执行顺序约束"]
|
||||
F --> G["不能直接持有 React state/effect"]
|
||||
|
||||
style A fill:#61DAFB,color:#000
|
||||
```
|
||||
|
||||
## 常见 Hook 模式
|
||||
|
||||
### 1. useFetch — 数据获取
|
||||
|
||||
```tsx
|
||||
interface UseFetchResult<T> {
|
||||
data: T | null;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
function useFetch<T>(url: string, options?: RequestInit): UseFetchResult<T> {
|
||||
const [data, setData] = useState<T | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
// ✅ 使用 useRef 跟踪组件挂载状态,避免卸载后 setState
|
||||
const mountedRef = useRef(true);
|
||||
|
||||
const execute = useCallback(async () => {
|
||||
if (!mountedRef.current) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch(url, options ?? {});
|
||||
if (!res.ok) throw new Error(res.statusText);
|
||||
const json: T = await res.json();
|
||||
if (mountedRef.current) setData(json);
|
||||
} catch (err) {
|
||||
if (mountedRef.current) setError(err as Error);
|
||||
} finally {
|
||||
if (mountedRef.current) setLoading(false);
|
||||
}
|
||||
}, [url, JSON.stringify(options)]);
|
||||
|
||||
useEffect(() => {
|
||||
execute();
|
||||
return () => { mountedRef.current = false; };
|
||||
}, [execute]);
|
||||
|
||||
return { data, loading, error, refetch: execute };
|
||||
}
|
||||
```
|
||||
|
||||
### 2. useDebounce — 防抖
|
||||
|
||||
```tsx
|
||||
interface DebounceResult<T> {
|
||||
debouncedValue: T;
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
function useDebounce<T>(value: T, delay: number): DebounceResult<T> {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
useEffect(() => {
|
||||
timerRef.current = setTimeout(() => setDebouncedValue(value), delay);
|
||||
return () => clearTimeout(timerRef.current);
|
||||
}, [value, delay]);
|
||||
|
||||
// ✅ 提供 cancel,外部可在需要时主动取消待触发的回调
|
||||
const cancel = useCallback(() => clearTimeout(timerRef.current), []);
|
||||
|
||||
return { debouncedValue, cancel };
|
||||
}
|
||||
|
||||
// 使用
|
||||
function SearchInput() {
|
||||
const [query, setQuery] = useState("");
|
||||
const { debouncedValue } = useDebounce(query, 300);
|
||||
|
||||
// 用 debouncedValue 发起 API 请求 —— 只在用户停止输入 300ms 后触发
|
||||
useEffect(() => { fetchData(debouncedValue); }, [debouncedValue]);
|
||||
|
||||
return <input value={query} onChange={e => setQuery(e.target.value)} />;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. useLocalStorage — 持久化状态
|
||||
|
||||
```tsx
|
||||
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void] {
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
// ✅ SSR 安全:检查 window 是否存在
|
||||
if (typeof window === "undefined") return initialValue;
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
} catch {
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
|
||||
const setValue = (value: T | ((prev: T) => T)) => {
|
||||
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
||||
setStoredValue(valueToStore);
|
||||
// ✅ SSR 安全
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleStorage = (e: StorageEvent) => {
|
||||
if (e.key === key && e.newValue !== null) {
|
||||
setStoredValue(JSON.parse(e.newValue));
|
||||
}
|
||||
};
|
||||
// ✅ SSR 安全
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("storage", handleStorage);
|
||||
return () => window.removeEventListener("storage", handleStorage);
|
||||
}
|
||||
}, [key]);
|
||||
|
||||
return [storedValue, setValue];
|
||||
}
|
||||
```
|
||||
|
||||
### 4. useIntersectionObserver — 视口检测
|
||||
|
||||
```tsx
|
||||
function useIntersectionObserver(
|
||||
options?: IntersectionObserverInit
|
||||
): [RefObject<HTMLDivElement | null>, boolean] {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
// ✅ 用 useMemo 缓存 options,避免每次渲染新建对象导致 observer 重建
|
||||
const memoizedOptions = useMemo(() => ({
|
||||
root: null,
|
||||
rootMargin: "0px",
|
||||
threshold: 0.1,
|
||||
...options,
|
||||
}), [JSON.stringify(options)]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
setIsVisible(entry.isIntersecting);
|
||||
}, memoizedOptions);
|
||||
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [memoizedOptions]);
|
||||
|
||||
return [ref, isVisible];
|
||||
}
|
||||
```
|
||||
|
||||
### 5. useMediaQuery — 媒体查询
|
||||
|
||||
```tsx
|
||||
function useMediaQuery(query: string): boolean {
|
||||
const [matches, setMatches] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia(query);
|
||||
setMatches(media.matches);
|
||||
|
||||
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
|
||||
media.addEventListener("change", handler);
|
||||
return () => media.removeEventListener("change", handler);
|
||||
}, [query]);
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
// 使用
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
```
|
||||
|
||||
### 6. usePrevious — 记录上一次值
|
||||
|
||||
```tsx
|
||||
function usePrevious<T>(value: T): T | undefined {
|
||||
const ref = useRef<T | undefined>();
|
||||
|
||||
// ✅ useEffect 在渲染完成后才执行,恰好获取"上一轮"的值
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
}, [value]);
|
||||
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
// 使用:判断值是否发生变化
|
||||
function Component({ count }) {
|
||||
const prevCount = usePrevious(count);
|
||||
return <p>{count === prevCount ? "不变" : "已变化!"}</p>;
|
||||
}
|
||||
```
|
||||
|
||||
### 7. useBoolean — 布尔状态简化器
|
||||
|
||||
```tsx
|
||||
function useBoolean(initialValue = false) {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
const toggle = useCallback(() => setValue(v => !v), []);
|
||||
const setTrue = useCallback(() => setValue(true), []);
|
||||
const setFalse = useCallback(() => setValue(false), []);
|
||||
|
||||
return { value, toggle, setTrue, setFalse };
|
||||
}
|
||||
|
||||
// 使用:替代手动写 { checked, setChecked }
|
||||
function ToggleButton() {
|
||||
const { value: on, toggle } = useBoolean();
|
||||
return <button onClick={toggle}>{on ? "ON" : "OFF"}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
## 常见陷阱与避坑
|
||||
|
||||
> [!danger] Hook 设计的 5 个经典陷阱
|
||||
> 以下是在生产环境中高频踩中的坑,务必警惕。
|
||||
|
||||
### 陷阱 1:闭包陷阱(Stale Closure)
|
||||
|
||||
```tsx
|
||||
// ❌ 问题:count 在 useCallback 创建时被捕获,永远是初始值
|
||||
function useCounter() {
|
||||
const [count, setCount] = useState(0);
|
||||
const double = useCallback(() => {
|
||||
console.log(count); // 始终是 0!
|
||||
setCount(count * 2);
|
||||
}, []); // 空依赖数组 → 捕获了初始状态
|
||||
}
|
||||
|
||||
// ✅ 解法 1:使用函数式 setState
|
||||
const double = useCallback(() => {
|
||||
setCount(c => c * 2); // 读取最新值
|
||||
}, []);
|
||||
|
||||
// ✅ 解法 2:将 count 加入依赖
|
||||
// const double = useCallback(() => {...}, [count]);
|
||||
```
|
||||
|
||||
### 陷阱 2:无限循环
|
||||
|
||||
```tsx
|
||||
// ❌ 问题:每次渲染都创建新对象,导致 useEffect 无限触发
|
||||
function BadComponent({ data }) {
|
||||
const [state, setState] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
setState([{ items: data }]); // 新数组 = 新引用
|
||||
}, [{ items: data }]); // ← 每次都是新对象,永远不等价
|
||||
}
|
||||
|
||||
// ✅ 解法:正确声明依赖项
|
||||
useEffect(() => {
|
||||
setState(prev => (prev[0]?.items === data ? prev : [{ items: data }]));
|
||||
}, [data]);
|
||||
```
|
||||
|
||||
### 陷阱 3:异步操作缺少清理
|
||||
|
||||
```tsx
|
||||
// ❌ 组件卸载后仍执行 setState → React 警告
|
||||
useEffect(() => {
|
||||
fetch("/api").then(res => setData(res.data)); // 卸载后回调仍会执行
|
||||
}, []);
|
||||
|
||||
// ✅ 使用 AbortController 取消请求
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
fetch("/api", { signal: controller.signal })
|
||||
.then(res => res.json())
|
||||
.then(setData)
|
||||
.catch(err => { if (err.name !== "AbortError") throw err; });
|
||||
|
||||
return () => controller.abort();
|
||||
}, []);
|
||||
```
|
||||
|
||||
### 陷阱 4:过度封装
|
||||
|
||||
```tsx
|
||||
// ❌ 为简单逻辑造 Hook,反而增加复杂度
|
||||
function useToggle(initial = false) {
|
||||
const [val, setVal] = useState(initial);
|
||||
return [val, () => setVal(v => !v), () => setVal(true), () => setVal(false)];
|
||||
// 返回值太长,调用方难以理解每个位置的含义
|
||||
}
|
||||
|
||||
// ✅ 直接内联或提取有意义的语义化 Hook
|
||||
const [open, setOpen] = useState(false);
|
||||
<button onClick={() => setOpen(!open)}>...</button>
|
||||
```
|
||||
|
||||
### 陷阱 5:副作用竞态(Race Condition)
|
||||
|
||||
```tsx
|
||||
// ❌ 快速切换搜索词时,旧请求可能晚于新请求返回
|
||||
function SearchResults({ query }) {
|
||||
const [results, setResults] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
searchAPI(query).then(setResults);
|
||||
}, [query]);
|
||||
|
||||
// ✅ 用 ref + 版本号追踪"当前请求"
|
||||
function useDebouncedSearch<T>(query: string, delay = 300): T | null {
|
||||
const [result, setResult] = useState<T | null>(null);
|
||||
const requestIdRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(async () => {
|
||||
const thisId = ++requestIdRef.current;
|
||||
const data = await searchAPI(query);
|
||||
if (thisId === requestIdRef.current) {
|
||||
setResult(data);
|
||||
}
|
||||
}, delay);
|
||||
return () => clearTimeout(timer);
|
||||
}, [query, delay]);
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
> [!tip] 自检清单
|
||||
> - Hook 的依赖数组是否包含了所有引用的外部变量?
|
||||
> - 异步操作中是否有清理机制防止"僵尸回调"?
|
||||
> - Hook 是否在条件语句中嵌套了?(违反 Rules of Hooks)
|
||||
> - Hook 的返回值类型是否清晰?优先用 interface/type 标注
|
||||
|
||||
## 组合模式与 HOC 的对比
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Composition["Hook 组合(推荐)"]
|
||||
C1["useBoolean"] --> C3["useModal"]
|
||||
C2["useClickOutside"] --> C3
|
||||
C3 --> C4["useForm"]
|
||||
C4 --> C5["DashboardPage"]
|
||||
style C3 fill:#61DAFB,color:#000
|
||||
style C5 fill:#4FC08D,color:#fff
|
||||
end
|
||||
|
||||
subgraph HOC["HOC 包装(不推荐)"]
|
||||
H1[Component] --> H2[HOC1]
|
||||
H2 --> H3[HOC2]
|
||||
H3 --> H4[HOC3]
|
||||
style H2 fill:#F5A87D,color:#000
|
||||
style H3 fill:#F5A87D,color:#000
|
||||
style H4 fill:#F5A87D,color:#000
|
||||
end
|
||||
```
|
||||
|
||||
### 实战:Hook 层层组合
|
||||
|
||||
```tsx
|
||||
// Layer 1: 基础 Hook
|
||||
function useBoolean(initial = false) { ... }
|
||||
function useClickOutside(ref, handler) { ... }
|
||||
|
||||
// Layer 2: 组合基础 Hook → 业务 Hook
|
||||
function useModal() {
|
||||
const { value: isOpen, toggle, setFalse: close } = useBoolean();
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Hook 中可以安全调用其他 Hook
|
||||
useClickOutside(overlayRef, close);
|
||||
|
||||
return { isOpen, open: toggle, close, overlayRef };
|
||||
}
|
||||
|
||||
// Layer 3: 业务页面
|
||||
function SettingsPanel() {
|
||||
const { isOpen, open, close, overlayRef } = useModal();
|
||||
return (
|
||||
<div ref={overlayRef}>
|
||||
<button onClick={open}>{isOpen ? "关闭设置" : "打开设置"}</button>
|
||||
{isOpen && <SettingsForm onClose={close} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> [!tip] Hook 设计的 S.O.L.I.D 原则
|
||||
> - **Single Responsibility**:一个 Hook 只做一件事(如 useFetch 只负责 fetch)
|
||||
> - **Open/Closed**:新需求加新 Hook,而非修改已有 Hook
|
||||
> - **Interface Segregation**:返回值尽量精确,不过度暴露内部细节
|
||||
> - **Dependency Inversion**:Hook 应依赖抽象(接口),而非具体实现
|
||||
|
||||
## 抽象层级参考
|
||||
|
||||
```tsx
|
||||
// Level 1: 基础 Hook(操作层面)
|
||||
function useClickOutside(ref: RefObject<HTMLElement>, handler: () => void) {}
|
||||
function useEventListener(target: any, event: string, fn: Function) {}
|
||||
|
||||
// Level 2: 业务 Hook(场景层面)
|
||||
function useModal() { return { isOpen, open, close, overlayRef: useClickOutside(...) } }
|
||||
function useForm(initialValues: FormValues) { return { values, errors, submit, reset } }
|
||||
|
||||
// Level 3: 领域 Hook(领域层面)
|
||||
function usePermission(role: Role) {} // 权限检查
|
||||
function usePagination(state: PaginationState) {} // 分页
|
||||
```
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[05-核心 Hooks]] — useState、useEffect 等基础 Hook,是理解自定义 Hook 的前置知识
|
||||
- [[06-性能优化 Hooks]] — useMemo、useCallback 在自定义 Hook 中的依赖优化实践
|
||||
Reference in New Issue
Block a user