Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← Web 开发 / Web Development

React 生态 / React Ecosystem

1. React 生态知识体系 / React Ecosystem Knowledge System

2. React 组件渲染、协调与数据流 / React Components, Rendering, and Data Flow

3. Hooks 深度剖析:状态、副作用与并发模式 / Hooks Deep Dive: State, Effects, and Concurrency

4. 路由、表单与组件架构 / Routing, Forms, and Component Architecture

5. React 状态管理:Redux Toolkit 与 Zustand / React State Management: Redux Toolkit and Zustand

6. Next.js App Router:路由、渲染与工程边界 / Next.js App Router, Routing, Rendering, and Engineering Boundaries

7. Next.js 数据获取、缓存与变更 / Next.js Data Fetching, Caching, and Mutations

8. TanStack Query 与服务端状态管理 / TanStack Query and Server State Management

9. React 测试、性能与生产工程 / React Testing, Performance, and Production Engineering

本页目录

Hooks 深度剖析:状态、副作用与并发模式 / Hooks Deep Dive: State, Effects, and Concurrency ​

📅 创建时间:2026-07-28 🏷️ 标签:#React #Hooks #useState #useEffect #useReducer #useRef #useMemo #React19 📚 前置知识:[[01-react-components-and-rendering]]


📋 本章目标 ​

  • 理解 Hook 底层存储机制(链表)以及"只在顶层调用"的真正原因
  • 掌握 useState 惰性初始化、函数式更新和不可变替换三件套
  • 能用 useReducer + 判别联合构建复杂状态机,并与 Context 组合做轻量状态管理
  • 区分 useRef 的三种使用模式:DOM 引用、可变值容器、实例变量
  • 把 useEffect 从"生命周期思维"转为"同步外部系统"思维,并识别不需要 Effect 的场景
  • 理解 useEffect / useLayoutEffect / useInsertionEffect 的执行时序差异和各自适用场景
  • 掌握 useMemo / useCallback 的性能权衡原则(不是默认就加)
  • 理解 useTransition / useDeferredValue 的并发更新机制和 Suspense 集成
  • 熟悉 React 19 四个新 Hook:use()、useActionState、useFormStatus、useOptimistic
  • 掌握自定义 Hook 的命名约定、返回值设计和稳定引用策略

第1部分:Hook 调用规则 —— 为什么"只在顶层调用" ​

1.1 链表:Hook 的底层存储模型 ​

React 在内部用单向链表保存每个组件实例的所有 Hook 状态。组件首次渲染时,每遇到一个 Hook 调用就在链表末尾追加一个节点;后续渲染按相同顺序遍历链表,将节点数据读出。这意味着 React 完全依赖调用顺序来匹配 Hook 与其状态。

┌─────────────────────────────────────────────────────────────┐
│                    组件 Fiber 的 Hook 链表                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  组件函数执行:                                               │
│                                                             │
│  useState('a') ──→ useState('b') ──→ useEffect(...)        │
│       │                │                  │                 │
│       ▼                ▼                  ▼                 │
│  ┌─────────┐     ┌─────────┐      ┌─────────────┐          │
│  │ state:  │     │ state:  │      │ effect:     │          │
│  │  'a'    │     │  'b'    │      │  callback   │          │
│  │ next ───┼────→│ next ───┼─────→│ next ──→ null│         │
│  └─────────┘     └─────────┘      └─────────────┘          │
│                                                             │
│  下一次渲染: 按相同顺序遍历链表,取回各自的状态               │
│  如果调用顺序变了(条件/循环中调用),节点对不上 → 💥        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

正是这个链表设计决定了第一条铁律:Hook 必须在组件或自定义 Hook 的顶层调用,不能放进条件、循环或普通回调。 否则调用顺序在两次渲染间变化,链表节点错位,可能导致状态串位甚至崩溃。

1.2 两条铁律的深层原因 ​

规则表面理解深层原因
只在顶层调用不要放在 if / for 里链表靠调用顺序索引,顺序变了 React 无法匹配
只在 React 函数中调用不要在普通 JS 函数里调Hook 的状态存储在 Fiber 节点上,普通函数没有对应的 Fiber

React 通过 ESLint 插件 eslint-plugin-react-hooks 在编译期检查这两条规则。但理解原理比依赖 lint 更重要——当你写自定义 Hook 时,你写的 Hook 调用了 useState/useEffect,这意味着你的自定义 Hook 也必须在顶层调用。

1.3 条件逻辑的正确放置位置 ​

tsx
function SearchPage({ enabled }: { enabled: boolean }) {
  const [query, setQuery] = useState('')

  // ✅ 条件放进 Effect 内部,而不是包住 Hook 调用
  useEffect(() => {
    if (!enabled) return                    // 提前返回 = 跳过同步
    document.title = query ? `搜索:${query}` : '搜索'
  }, [enabled, query])

  return (/* ... */)
}
1
2
3
4
5
6
7
8
9
10
11

第2部分:useState 深度解析 ​

2.1 惰性初始化:避免重复昂贵计算 ​

useState(initialValue) 中的 initialValue 在每次渲染时都会执行(只是除首次外结果被丢弃)。如果初始值需要从 localStorage 读取、需要复杂计算,应该用惰性初始化——传入一个函数:

tsx
// ❌ 每次渲染都读取 localStorage,尽管只有首次用到
const [todos, setTodos] = useState(
  JSON.parse(localStorage.getItem('todos') ?? '[]')
)

// ✅ 传入函数:React 只在首次渲染时调用它
const [todos, setTodos] = useState(() => {
  const saved = localStorage.getItem('todos')
  return saved ? JSON.parse(saved) : []
})
1
2
3
4
5
6
7
8
9
10

惰性初始化的函数必须是纯函数——不能有副作用,React 在严格模式下可能调用两次以检测纯度。

2.2 函数式更新:避免陈旧闭包 ​

当新状态依赖旧状态时,直接传值可能读到过期的值:

tsx
// ❌ 风险:如果短时间内多次 setCount,每次拿到的 prev 可能都是旧的
function handleTriple() {
  setCount(count + 1)
  setCount(count + 1)    // 这个 count 和上一行拿到的是同一个值
  setCount(count + 1)    // 三行都用的是快照值 → 最终只 +1
}

// ✅ 函数式更新:React 把最新的 pending state 传给你
function handleTriple() {
  setCount(prev => prev + 1)
  setCount(prev => prev + 1)
  setCount(prev => prev + 1)    // 每次拿到上一步的结果 → 最终 +3
}
1
2
3
4
5
6
7
8
9
10
11
12
13

函数式更新在以下场景特别关键:

  • 连续多次 setState(批量更新期间)
  • useEffect 依赖数组为空但需要 state 的最新值
  • 用 useCallback 包裹的回调中引用了 state

2.3 对象和数组的不可变替换 ​

React 用 Object.is() 做浅比较。直接 mutate 对象/数组不会触发重渲染:

tsx
// ❌ 直接 push:引用没变 → React 认为没变化 → 不重渲染
todos.push(newTodo)
setTodos(todos)

// ✅ 创建新数组:引用变了 → React 检测到变化 → 重渲染
setTodos([...todos, newTodo])

// ✅ 对象同理:展开 + 覆盖
setUser(prev => ({ ...prev, name: 'Alice' }))

// ✅ 深层嵌套考虑 useImmer 或结构化克隆
setConfig(prev => ({
  ...prev,
  theme: { ...prev.theme, dark: !prev.theme.dark }
}))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

第3部分:useReducer 完整实战 ​

3.1 为什么需要 Reducer ​

当多个事件以不同方式修改同一块状态,或状态更新逻辑复杂到难以用若干个 useState 表达时,useReducer 将状态转移逻辑集中到一个纯函数中:

┌─────────────────────────────────────────────────────────────┐
│                   useReducer 数据流                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   用户操作                                                   │
│   ┌──────┐    dispatch({ type: 'added', title: '...' })    │
│   │ 点击  │ ─────────────────────────────────────┐          │
│   └──────┘                                      │          │
│                                                 ▼          │
│   网络回调                                  ┌──────────┐   │
│   ┌──────┐    dispatch({ type: 'loaded' })  │          │   │
│   │ 响应  │ ──────────────────────────────→ │ Reducer  │   │
│   └──────┘                                  │ (纯函数)  │   │
│                                                 │          │
│   定时器                                       │          │
│   ┌──────┐    dispatch({ type: 'tick' })       │          │
│   │ 计时  │ ──────────────────────────────────┘          │
│   └──────┘                                      │          │
│                                                 ▼          │
│                                            新 State        │
│                                                             │
│   关键:所有修改都通过 dispatch → reducer,单向流动          │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

3.2 用判别联合设计 Action 类型 ​

TypeScript 的 discriminated union 让每种 action 携带不同的 payload,类型安全:

tsx
type Todo = { id: string; title: string; done: boolean }

type Action =
  | { type: 'added'; title: string }
  | { type: 'removed'; id: string }
  | { type: 'toggled'; id: string }
  | { type: 'edited'; id: string; title: string }
  | { type: 'cleared_completed' }

function todoReducer(state: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case 'added':
      return [...state, { id: crypto.randomUUID(), title: action.title, done: false }]
    case 'removed':
      return state.filter(t => t.id !== action.id)
    case 'toggled':
      return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t)
    case 'edited':
      return state.map(t => t.id === action.id ? { ...t, title: action.title } : t)
    case 'cleared_completed':
      return state.filter(t => !t.done)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

Reducer 必须纯净。crypto.randomUUID() 这类副作用需放在 dispatch 之前生成好 ID,再写入 action。

3.3 与 Immer 集成 ​

深层嵌套状态的手动展开很繁琐,Immer 的 produce 让你用 mutable 写法生成 immutable 结果:

tsx
import { produce } from 'immer'

type State = { user: { profile: { name: string; bio: string } }; posts: Post[] }

function reducer(state: State, action: Action): State {
  return produce(state, draft => {
    switch (action.type) {
      case 'update_bio':
        draft.user.profile.bio = action.bio   // 像 mutable 一样写
        break
      case 'rename':
        draft.user.profile.name = action.name
        break
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Immer 也提供 useImmerReducer 快捷封装,省去手动调用 produce。

3.4 与 Context 组合做轻量状态管理 ​

对于中型应用,useReducer + Context 是 Redux/Zustand 的轻量替代:

tsx
// 分别导出 state 和 dispatch,避免不必要渲染
const TodosContext    = createContext<Todo[]>([])
const DispatchContext = createContext<React.Dispatch<Action>>(() => {})

export function TodosProvider({ children }: { children: React.ReactNode }) {
  const [todos, dispatch] = useReducer(todoReducer, [])

  return (
    <DispatchContext.Provider value={dispatch}>
      <TodosContext.Provider value={todos}>
        {children}
      </TodosContext.Provider>
    </DispatchContext.Provider>
  )
}

// 消费者只需读取自己需要的 Context
export function useTodos()    { return useContext(TodosContext) }
export function useDispatch() { return useContext(DispatchContext) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

为什么拆成两个 Context? 只 dispatch 的组件不需要在 todos 变化时重渲染。按职责拆分 Context 是以最小粒度订阅的核心技巧。

3.5 useReducer vs useState 选择标准 ​

┌─────────────────────────────────────────────────────────────┐
│                useReducer vs useState 选择决策树              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  你的状态是:                                                 │
│                                                             │
│  ┌──────────────────┐                                       │
│  │ 单个简单值        │ ───→ useState ✅                      │
│  │ (字符串/数字/bool) │                                      │
│  └──────────────────┘                                       │
│                                                             │
│  ┌──────────────────┐                                       │
│  │ 多个独立值         │ ───→ 多个 useState ✅                 │
│  │ (互不依赖)         │                                      │
│  └──────────────────┘                                       │
│                                                             │
│  ┌──────────────────┐                                       │
│  │ 多个值互相关联     │                                      │
│  │ 更新逻辑复杂       │ ───→ useReducer ✅                   │
│  │ 下一个状态依赖     │                                      │
│  │ 上一个状态         │                                      │
│  └──────────────────┘                                       │
│                                                             │
│  ┌──────────────────┐                                       │
│  │ 需要测试状态转移   │ ───→ useReducer ✅                   │
│  │ reducer 可独立     │    (reducer 是纯函数,易于单测)       │
│  │ 于组件测试         │                                      │
│  └──────────────────┘                                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

第4部分:useRef 的三种模式 ​

4.1 模式一:DOM 引用 ​

最经典的用法 —— 拿到原生 DOM 节点的直接引用:

tsx
function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    inputRef.current?.focus()          // 挂载后自动聚焦
  }, [])

  return <input ref={inputRef} placeholder="自动聚焦" />
}
1
2
3
4
5
6
7
8
9

4.2 模式二:可变值容器(不触发渲染) ​

useRef 创建的 { current: ... } 对象在组件整个生命周期中保持同一个引用。修改 .current 不会触发重渲染,适合存储不需要驱动 UI 的数据:

tsx
function Timer() {
  const intervalRef = useRef<number | null>(null)
  const [count, setCount] = useState(0)

  const start = () => {
    if (intervalRef.current !== null) return       // 防止重复启动
    intervalRef.current = window.setInterval(() => {
      setCount(c => c + 1)                          // 用 useState 驱动 UI
    }, 1000)
  }

  const stop = () => {
    if (intervalRef.current === null) return
    clearInterval(intervalRef.current)
    intervalRef.current = null
  }

  // 组件卸载时清理
  useEffect(() => () => { stop() }, [])

  return (/* ... */)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这里 intervalRef 存的是 timer ID——它不需要显示在 UI 上,但需要在 start 和 stop 之间共享。如果用 useState 存 ID,每次修改都触发不必要的重渲染。

4.3 模式三:实例变量模式(存储 previous value) ​

追踪上一次渲染时的值,常用于比较变化或实现动画:

tsx
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>()

  useEffect(() => {
    ref.current = value                     // Effect 在渲染后执行,存的是"上一轮"的值
  })

  return ref.current                        // 返回的是还没更新的旧值
}

// 使用
function Chat({ roomId }: { roomId: string }) {
  const prevRoomId = usePrevious(roomId)

  useEffect(() => {
    if (prevRoomId !== roomId) {
      console.log(`切换房间: ${prevRoomId} → ${roomId}`)
    }
  }, [roomId, prevRoomId])
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这个模式的原理很精妙:ref.current = value 在 Effect 中执行,发生在 DOM 更新之后。组件函数体中 return ref.current 在渲染阶段执行,此时 ref 里存的是上一轮 Effect 写入的值。

4.4 forwardRef + useImperativeHandle ​

当需要向父组件暴露子组件的 DOM 或方法时:

tsx
interface ImperativeHandle {
  focus: () => void
  scrollToTop: () => void
}

const FancyInput = forwardRef<ImperativeHandle, { label: string }>(
  function FancyInput({ label }, ref) {
    const inputRef    = useRef<HTMLInputElement>(null)
    const containerRef = useRef<HTMLDivElement>(null)

    useImperativeHandle(ref, () => ({
      focus: () => inputRef.current?.focus(),
      scrollToTop: () => containerRef.current?.scrollTo({ top: 0, behavior: 'smooth' }),
    }), [])  // 依赖数组为空:方法引用稳定不变

    return (
      <div ref={containerRef}>
        <label>{label}</label>
        <input ref={inputRef} />
      </div>
    )
  }
)

// 父组件
function Parent() {
  const fancyRef = useRef<ImperativeHandle>(null)
  return (
    <>
      <FancyInput ref={fancyRef} label="姓名" />
      <button onClick={() => fancyRef.current?.focus()}>聚焦</button>
    </>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

useImperativeHandle 让你控制暴露的接口,而不是把整个 DOM 节点暴露出去。这在封装第三方组件库或复杂 UI 逻辑时尤其有用。


第5部分:useEffect 的正确心智模型 ​

5.1 核心思想:同步外部系统,不是"生命周期" ​

React 文档强调:Effect 是用来让组件与外部系统保持同步的。外部系统包括:

  • 浏览器 API(document.title、localStorage、history)
  • 第三方库(图表、地图 SDK、视频播放器)
  • 网络连接(WebSocket、EventSource)
  • 全局事件订阅
┌─────────────────────────────────────────────────────────────┐
│                  Effect 同步模型                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│    React 状态 ──────────────────────────→ 外部系统           │
│   (state, props)         同步方向        (DOM 属性,          │
│       │                                  网络连接,           │
│       │                                  第三方实例)          │
│       │                                                     │
│        ← 外部系统变化通过事件/回调 → setState → 触发同步      │
│                                                             │
│   useEffect(() => {                                         │
│     /* 建立同步 */                                           │
│     return () => { /* 清理上一轮同步 */ }                    │
│   }, [/* 依赖:当这些值变化时重新同步 */])                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

5.2 清理函数:防止内存泄漏和状态错乱 ​

每次 Effect 重新执行前(依赖变化或组件卸载),React 会先用旧依赖执行清理函数,再用新依赖执行 setup:

tsx
function ChatRoom({ roomId }: { roomId: string }) {
  const [messages, setMessages] = useState<string[]>([])

  useEffect(() => {
    const ws = new WebSocket(`wss://chat.example.com/${roomId}`)

    ws.addEventListener('message', (event) => {
      setMessages(prev => [...prev, event.data])
    })

    return () => {
      ws.close()                              // 切换到其他房间时关闭旧连接
    }
  }, [roomId])
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
渲染 roomId='A' → 连接 A
渲染 roomId='B' → 先 close A → 再连接 B  (由清理函数 + 新 setup 完成)
组件卸载       → close B
1
2
3

5.3 依赖数组的工作原理 ​

React 在每次渲染后比较依赖数组中的每个值(使用 Object.is())。只要有一个值变了,就触发 Effect 重新执行。

常见误区:

tsx
// ❌ 空依赖数组 + 内部引用 state → 陈旧闭包
useEffect(() => {
  const timer = setInterval(() => {
    console.log(count)                 // count 永远是首次渲染的值
  }, 1000)
  return () => clearInterval(timer)
}, [])

// ✅ 方案1:把 count 加入依赖
useEffect(() => {
  const timer = setInterval(() => { console.log(count) }, 1000)
  return () => clearInterval(timer)
}, [count])                             // 但每秒重建 interval,不优雅

// ✅ 方案2:函数式更新,不依赖外部 count
useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => console.log(c) || c + 1)
  }, 1000)
  return () => clearInterval(timer)
}, [])                                  // 依赖数组为空,interval 一次创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

5.4 严格模式下的双重调用 ​

React 18+ 在开发环境的 StrictMode 下会挂载 → 卸载 → 重新挂载每个组件。这会导致 Effect 执行两次(setup → cleanup → setup)。这不是 bug,而是一个压力测试,帮你提前发现缺少清理逻辑的 Effect。

如果你的 Effect 在 StrictMode 下异常(如重复连接 WebSocket),说明清理函数不够完善。

5.5 何时不该用 Effect ​

React 官方列出了几种不需要 Effect 的场景,这是从"生命周期思维"转变的关键:

┌─────────────────────────────────────────────────────────────┐
│                    不需要 Effect 的场景                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  场景1:从 props/state 计算派生值                             │
│  ❌ const [fullName, setFullName] = useState('')            │
│     useEffect(() => {                                       │
│       setFullName(firstName + ' ' + lastName)               │
│     }, [firstName, lastName])                               │
│  ✅ const fullName = firstName + ' ' + lastName             │
│     直接在渲染中计算,不需要额外的状态 + Effect               │
│                                                             │
│  场景2:响应用户事件                                         │
│  ❌ useEffect(() => { /* 发请求 */ }, [submitted])           │
│  ✅ function handleSubmit() { /* 发请求 */ }                │
│     用户点击是事件,不是状态同步,应在事件处理器中处理         │
│                                                             │
│  场景3:重置子组件树                                         │
│  ❌ useEffect(() => { resetForm() }, [userId])               │
│  ✅ <ProfileForm key={userId} />                            │
│     key 变化时 React 会卸载旧组件、挂载新组件,自然达到重置   │
│                                                             │
│  场景4:缓存昂贵计算                                         │
│  ❌ useEffect(() => { setResult(heavyCalc(data)) }, [data])  │
│  ✅ const result = useMemo(() => heavyCalc(data), [data])    │
│     useMemo 在渲染期间计算,不会造成额外的提交 + 渲染         │
│                                                             │
│  场景5:数据获取                                             │
│  ❌ useEffect 里手写 fetch + loading + error                │
│  ✅ 使用 TanStack Query / SWR / RTK Query                   │
│     它们处理缓存、去重、重试、竞态、后台刷新等               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

5.6 竞态处理 ​

手写请求 Effect 时,必须处理旧请求晚于新请求返回的问题:

tsx
useEffect(() => {
  let ignore = false                            // 闭包标记

  async function fetchUsers() {
    const res = await fetch(`/api/users?q=${query}`)
    const data = await res.json()
    if (!ignore) setUsers(data)                 // 只有最新请求的结果才被采用
  }

  fetchUsers()

  return () => { ignore = true }                // 下一次 Effect 执行前标记为忽略
}, [query])
1
2
3
4
5
6
7
8
9
10
11
12
13

使用 AbortController 是更彻底的方案(可以从网络层取消请求),但布尔标记在大多数场景下已经足够。


第6部分:useLayoutEffect vs useEffect vs useInsertionEffect ​

6.1 三者的执行时序 ​

┌─────────────────────────────────────────────────────────────┐
│              渲染 → 提交 流程中的 Hook 执行点                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              渲染阶段 (Render Phase)                  │   │
│  │   React 调用组件函数,计算 Virtual DOM              │   │
│  │   useInsertionEffect 的 setup 在此排队              │   │
│  └──────────────────────┬──────────────────────────────┘   │
│                         │                                   │
│                         ▼                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              DOM 变更 (Mutation)                      │   │
│  │   React 将 Virtual DOM 写进真实 DOM                 │   │
│  └──────────────────────┬──────────────────────────────┘   │
│                         │                                   │
│                         ▼                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │         useInsertionEffect 执行                       │   │
│  │   在 DOM 变更之后、layout effect 之前                  │   │
│  │   用途:CSS-in-JS 库插入样式规则                       │   │
│  └──────────────────────┬──────────────────────────────┘   │
│                         │                                   │
│                         ▼                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │         useLayoutEffect 执行                          │   │
│  │   在浏览器绘制之前同步执行                            │   │
│  │   用途:读取 DOM 布局 + 同步修改,避免闪烁            │   │
│  │   ⚠️ 阻塞绘制,长任务会让用户看到白屏                  │   │
│  └──────────────────────┬──────────────────────────────┘   │
│                         │                                   │
│                         ▼                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              浏览器绘制 (Paint)                       │   │
│  │   像素呈现在屏幕上                                   │   │
│  └──────────────────────┬──────────────────────────────┘   │
│                         │                                   │
│                         ▼                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │           useEffect 执行                              │   │
│  │   在浏览器绘制之后异步执行                            │   │
│  │   用途:大多数同步逻辑、网络请求、订阅                │   │
│  │   ✅ 不阻塞绘制,用户看到 UI 后才执行                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

6.2 各自适用场景 ​

Hook何时用典型场景
useEffect绝大多数情况数据获取、订阅、手动改 DOM 属性(不影响布局)、日志
useLayoutEffect需要在绘制前同步测量/修改 DOM测量元素尺寸后定位 tooltip、同步滚动位置、避免闪烁的 DOM 操作
useInsertionEffectCSS-in-JS 库作者在 DOM 变更后、布局计算前动态注入 <style> 规则
tsx
// useLayoutEffect 经典场景:tooltip 定位
function Tooltip({ targetRef }: { targetRef: RefObject<HTMLElement> }) {
  const tooltipRef = useRef<HTMLDivElement>(null)
  const [pos, setPos] = useState({ x: 0, y: 0 })

  useLayoutEffect(() => {
    const target = targetRef.current
    const tooltip = tooltipRef.current
    if (!target || !tooltip) return

    const targetRect = target.getBoundingClientRect()
    // 测量 + 同步设置位置,在绘制前完成 → 用户不会看到 tooltip"跳一下"
    setPos({
      x: targetRect.left,
      y: targetRect.bottom + 8,
    })
  }, [targetRef])

  return <div ref={tooltipRef} style={{ position: 'fixed', left: pos.x, top: pos.y }}>...</div>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

经验法则:默认用 useEffect。只有当 UI 出现可感知的闪烁或布局跳动,再考虑改成 useLayoutEffect。


第7部分:useMemo 和 useCallback 的正确姿势 ​

7.1 核心原则:不是"默认就加" ​

useMemo 和 useCallback 本身有开销:它们创建闭包、维护依赖数组、在每次渲染时做依赖比较。在简单计算或轻量组件上,加 memo 的开销可能比不加更大。

┌─────────────────────────────────────────────────────────────┐
│              useMemo / useCallback 决策流程                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  这个值/函数是否:                                            │
│                                                             │
│  1. 计算昂贵? (循环 > 1000 次 / 递归 / 大数组排序)           │
│     └── 否 → 不加 memo ✅                                   │
│     └── 是 → 继续判断 ↓                                     │
│                                                             │
│  2. 被传给用 React.memo 包裹的子组件?                        │
│     └── 是 → 需要用 useMemo / useCallback 保持引用稳定       │
│     └── 否 → 可以不加                                       │
│                                                             │
│  3. 是 useEffect 的依赖?                                    │
│     └── 是 → 如果每次渲染都变会导致 Effect 频繁执行          │
│             用 useCallback 包裹可以避免                      │
│     └── 否 → 可以不加                                       │
│                                                             │
│  4. 是自定义 Hook 的返回值?                                  │
│     └── 是 → 应该用 useCallback 包裹,调用方无法控制         │
│             你的 Hook 内部是否每次都创建新函数               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

7.2 useMemo 实战:缓存昂贵派生值 ​

tsx
function ProductList({ products, minPrice, maxPrice, sortBy }: Props) {
  // ✅ useMemo:过滤 + 排序是 O(n log n),数据量大时值得缓存
  const filtered = useMemo(() => {
    return products
      .filter(p => p.price >= minPrice && p.price <= maxPrice)
      .sort((a, b) => {
        switch (sortBy) {
          case 'price-asc':  return a.price - b.price
          case 'price-desc': return b.price - a.price
          case 'name':       return a.name.localeCompare(b.name)
          default:           return 0
        }
      })
  }, [products, minPrice, maxPrice, sortBy])

  return <ul>{filtered.map(p => <ProductItem key={p.id} product={p} />)}</ul>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

7.3 useCallback + React.memo 组合拳 ​

单用 useCallback 不会阻止子组件重渲染——还需要子组件用 React.memo 包装:

tsx
const ExpensiveChild = React.memo(function ExpensiveChild({
  onSelect,
}: {
  onSelect: (id: string) => void
}) {
  // 只有 onSelect 引用变化时才重渲染
  return (/* 昂贵的渲染 */)
})

function Parent() {
  const [items, setItems] = useState<Item[]>([])

  // ✅ useCallback 保证 onSelect 引用稳定
  const handleSelect = useCallback((id: string) => {
    setItems(prev => prev.map(item =>
      item.id === id ? { ...item, selected: true } : item
    ))
  }, [])  // 使用函数式更新,依赖数组可以为空

  return (
    <>
      {items.map(item => (
        <ExpensiveChild key={item.id} onSelect={handleSelect} />
      ))}
    </>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

7.4 自定义 Hook 中的 useCallback ​

自定义 Hook 返回的函数应该用 useCallback 包裹——调用方可能把这些函数作为 useEffect 依赖或传给 memo 组件:

tsx
function useSearch(query: string) {
  const [results, setResults] = useState<Result[]>([])

  const search = useCallback(async (q: string) => {
    const res = await fetch(`/api/search?q=${q}`)
    setResults(await res.json())
  }, [])

  const clear = useCallback(() => {
    setResults([])
  }, [])

  return { results, search, clear }
  // search 和 clear 的引用永远不变 → 调用方可以安全地放入依赖数组
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

如果你不确定要不要加 useCallback:先不加。用 React DevTools Profiler 实测。发现性能瓶颈后再加,而不是提前优化。


第8部分:useTransition 和 useDeferredValue —— 并发渲染 ​

8.1 React 18 并发模式的核心思想 ​

并发渲染允许 React 中断正在进行的渲染来处理更高优先级的更新(如用户输入),然后再回来继续低优先级的渲染。这让 UI 始终保持响应。

useTransition 和 useDeferredValue 是这一机制的 API 入口。

8.2 useTransition:标记非紧急更新 ​

tsx
function SearchPage() {
  const [query, setQuery] = useState('')
  const [isPending, startTransition] = useTransition()

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // 紧急更新:立即更新输入框的值
    setQuery(e.target.value)

    // 非紧急更新:可以被中断,不会阻塞输入
    startTransition(() => {
      setFilteredResults(filterAndSort(allItems, e.target.value))
    })
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending && <Spinner />}
      <ResultList results={filteredResults} />
    </>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌─────────────────────────────────────────────────────────────┐
│                 useTransition 工作原理                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   用户快速输入 "react":                                      │
│                                                             │
│   输入 'r':  setQuery('r')  +  startTransition(过滤 'r')    │
│   输入 're': setQuery('re') +  startTransition(过滤 're')   │
│               ↑ 紧急更新中断了正在进行的过滤 'r' 的渲染       │
│   输入 'rea': ...                                            │
│                                                             │
│   最终: React 丢弃被中断的中间渲染,只完成最后一次过滤       │
│   用户始终看到流畅的输入 → 无卡顿                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

8.3 useDeferredValue:延迟一个值的更新 ​

当 props 来自外部(无法用 startTransition 包裹),用 useDeferredValue 延迟该值的同步:

tsx
function SearchResults({ query }: { query: string }) {
  // deferredQuery 会"滞后"于 query:query 变化时先渲染旧结果,
  // 等 React 空闲了再更新 deferredQuery 并渲染新结果
  const deferredQuery = useDeferredValue(query)
  const results = useMemo(() => filterItems(allItems, deferredQuery), [deferredQuery])

  const isStale = query !== deferredQuery   // 显示的是旧结果时提示

  return (
    <div style={{ opacity: isStale ? 0.5 : 1 }}>
      {results.map(r => <ResultItem key={r.id} item={r} />)}
    </div>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
特性useTransitionuseDeferredValue
控制方你有 setState 的控制权props 来自外部,你无法控制更新时机
返回[isPending, startTransition]延迟后的值
用法startTransition(() => setX(v))const d = useDeferredValue(v)
适用场景你触发状态更新状态由父组件或外部源驱动

8.4 与 Suspense 集成 ​

并发 Hook 和 Suspense 天然搭配:

tsx
function App() {
  const [tab, setTab] = useState('home')
  const [isPending, startTransition] = useTransition()

  function switchTab(next: string) {
    startTransition(() => setTab(next))
  }

  return (
    <>
      <TabBar onSwitch={switchTab} isPending={isPending} />
      <Suspense fallback={<Skeleton />}>
        {tab === 'home'    && <HomeFeed />}
        {tab === 'profile' && <ProfilePage />}
      </Suspense>
    </>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

startTransition 包裹 setTab 后,React 不会在切换 tab 时隐藏已加载的内容去显示 Suspense fallback——而是保持旧 UI 直到新内容就绪,避免"闪一下骨架屏"的不良体验。


第9部分:React 19 新 Hook ​

9.1 use() —— 在渲染中读取 Promise 和 Context ​

React 19 的 use() 是一个特殊的 Hook,它可以在条件语句和循环中调用(打破了传统 Hook 规则,因为它内部不依赖链表索引):

tsx
async function fetchUser(id: string) {
  const res = await fetch(`/api/users/${id}`)
  return res.json()
}

function UserProfile({ userId }: { userId: string }) {
  // use() 读取 Promise:React 会"暂停"渲染直到 Promise resolve
  // 需要外层 <Suspense> 提供 fallback
  const user = use(fetchUser(userId))

  // use() 也能读取 Context(可以在条件中)
  const theme = userId === 'admin'
    ? use(AdminThemeContext)
    : use(DefaultThemeContext)

  return <div style={{ color: theme.color }}>{user.name}</div>
}

// 外层必须包裹 Suspense
function App() {
  return (
    <Suspense fallback={<Loading />}>
      <UserProfile userId="123" />
    </Suspense>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

use() 和 useContext() 的关键区别:

  • use() 可以在条件/循环中使用;useContext() 不行
  • use() 读 Promise 时需要 Suspense;useContext() 不需要

9.2 useActionState —— 表单 Action 状态管理 ​

替代了传统的 useState({ error, loading }) + try/catch 模式,直接与 Server Actions 或 async 函数集成:

tsx
import { useActionState } from 'react'

async function updateProfile(prevState: State, formData: FormData): Promise<State> {
  const name = formData.get('name') as string
  if (!name) return { error: '名字不能为空' }

  try {
    await saveToServer({ name })
    return { message: '保存成功' }
  } catch {
    return { error: '保存失败,请重试' }
  }
}

function EditProfile() {
  const [state, submitAction, isPending] = useActionState(updateProfile, { message: '' })

  return (
    <form action={submitAction}>
      <input name="name" />
      <button type="submit" disabled={isPending}>
        {isPending ? '保存中...' : '保存'}
      </button>
      {state.error   && <p className="error">{state.error}</p>}
      {state.message && <p className="success">{state.message}</p>}
    </form>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

9.3 useFormStatus —— 读取表单提交状态 ​

在表单内部的子组件中读取父 <form> 的提交状态,无需 prop drilling:

tsx
import { useFormStatus } from 'react-dom'

function SubmitButton() {
  const { pending, data, method, action } = useFormStatus()

  return (
    <button type="submit" disabled={pending}>
      {pending ? '提交中...' : '提交'}
    </button>
  )
}

function MyForm() {
  return (
    <form action={submitAction}>
      <input name="email" />
      <SubmitButton />   {/* 自动读取外层 form 的 pending 状态 */}
    </form>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

9.4 useOptimistic —— 乐观更新 ​

在服务器响应之前就更新 UI,如果最终失败则回滚:

tsx
import { useOptimistic } from 'react'

function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
  const [optimisticTodos, addOptimistic] = useOptimistic(
    initialTodos,
    (state, newTodo: Todo) => [...state, newTodo]
  )

  async function handleAdd(formData: FormData) {
    const title = formData.get('title') as string
    const tempTodo: Todo = { id: `temp-${Date.now()}`, title, done: false }

    // 立刻更新 UI(乐观)
    addOptimistic(tempTodo)

    // 发送真实请求
    await createTodoOnServer(title)
    // 如果请求失败,React 自动回滚 optimisticTodos 到 initialTodos
  }

  return (
    <>
      <form action={handleAdd}>
        <input name="title" />
        <button type="submit">添加</button>
      </form>
      <ul>
        {optimisticTodos.map(t => (
          <li key={t.id} style={{ opacity: t.id.startsWith('temp') ? 0.5 : 1 }}>
            {t.title}
          </li>
        ))}
      </ul>
    </>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

React 19 新 Hook 一览:

┌─────────────────────────────────────────────────────────────┐
│                   React 19 新增 Hook 总结                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  use()                                                      │
│  ├── 读取 Promise  → 配合 Suspense,声明式数据获取           │
│  ├── 读取 Context  → 可在条件/循环中使用                     │
│  └── 打破"只在顶层调用"规则                                 │
│                                                             │
│  useActionState(formAction, initialState)                    │
│  ├── 返回 [state, submitAction, isPending]                  │
│  └── 替代手动 error/loading 状态管理                        │
│                                                             │
│  useFormStatus()                                             │
│  ├── 返回 { pending, data, method, action }                 │
│  └── 子组件无需 prop drilling 即可读取 form 状态             │
│                                                             │
│  useOptimistic(state, updateFn)                              │
│  ├── 返回 [optimisticState, addOptimistic]                  │
│  └── 乐观更新 + 自动回滚                                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

第10部分:自定义 Hook 设计模式 ​

10.1 本质:复用状态逻辑,而非共享状态 ​

自定义 Hook 复用的是状态逻辑 + 同步协议,而不是共享同一份状态。每个调用自定义 Hook 的组件拥有独立的 state 和 effect 实例——就和直接调用 useState/useEffect 一样。

10.2 命名约定与返回值设计 ​

tsx
// ✅ 命名以 use 开头(React 要求,ESLint 也依赖这个规则)
function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(() => window.matchMedia(query).matches)

  useEffect(() => {
    const mql = window.matchMedia(query)
    const handler = (e: MediaQueryListEvent) => setMatches(e.matches)
    mql.addEventListener('change', handler)
    return () => mql.removeEventListener('change', handler)
  }, [query])

  return matches
}
1
2
3
4
5
6
7
8
9
10
11
12
13

返回值设计原则:

  • 单个值(如 useOnlineStatus → boolean):直接 return
  • 多个值(如 useSearch → { results, search, clear }):返回对象,调用方按需解构
  • 类 useState 的二元组(如 useToggle → [value, toggle]):用于调用方熟悉的心智模型

10.3 稳定引用策略 ​

自定义 Hook 返回的对象/函数必须考虑引用稳定性:

tsx
// ❌ 每次渲染都返回新对象 → 调用方 useEffect 会频繁触发
function useWindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 })
  // ...
  return { width: size.width, height: size.height }
}

// ✅ 方案1:useMemo 稳定返回值的引用
function useWindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 })
  // ...
  return useMemo(() => ({ width: size.width, height: size.height }), [size.width, size.height])
}

// ✅ 方案2:拆成两个独立的 Hook 或返回值
function useWindowSize() {
  const [width, setWidth]   = useState(0)
  const [height, setHeight] = useState(0)
  // ...
  return { width, height }   // 基本类型的引用本身就是稳定的
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

10.4 典型模式汇总 ​

┌─────────────────────────────────────────────────────────────┐
│                 自定义 Hook 常见模式                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  模式1: 封装浏览器 API                                       │
│  useOnlineStatus, useMediaQuery, useLocalStorage,           │
│  useGeolocation, useClipboard                               │
│                                                             │
│  模式2: 封装副作用 + 清理                                    │
│  useEventListener, useInterval, useTimeout,                 │
│  useWebSocket, useIntersectionObserver                      │
│                                                             │
│  模式3: 数据获取                                             │
│  useFetch, useQuery, useMutation                            │
│  注意:生产环境优先 TanStack Query / SWR                    │
│                                                             │
│  模式4: 状态机 / 布尔开关                                    │
│  useToggle, useBoolean, useCounter, useUndo                 │
│                                                             │
│  模式5: 组合 Context                                         │
│  useAuth, useTheme, useTodos → 封装 useContext 调用         │
│  提供更好的错误提示(不在 Provider 内时 throw)             │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

10.5 实战:组合多个基础 Hook 构建高级 Hook ​

tsx
function useDebouncedSearch(delay = 300) {
  const [query, setQuery] = useState('')
  const debouncedQuery = useDebounce(query, delay)

  const {
    data: results,
    isLoading,
    error,
  } = useQuery({
    queryKey: ['search', debouncedQuery],
    queryFn: () => fetchResults(debouncedQuery),
    enabled: debouncedQuery.length > 0,
  })

  return { query, setQuery, results, isLoading, error }
}

// 这个 Hook 组合了:
// - useState (query 状态)
// - useDebounce (防抖,可能是另一个自定义 Hook)
// - useQuery (TanStack Query,数据获取)
// 对外暴露统一的接口,调用方只需一行代码
function SearchBox() {
  const { query, setQuery, results, isLoading } = useDebouncedSearch(300)
  return (/* ... */)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

核心总结 ​

┌─────────────────────────────────────────────────────────────┐
│              React Hooks 心智模型总结                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   渲染阶段 (Render)    → useState, useReducer, useMemo,     │
│                          useCallback, useRef (创建对象)      │
│                                                             │
│   DOM 变更后,绘制前   → useInsertionEffect, useLayoutEffect │
│                                                             │
│   浏览器绘制后          → useEffect                          │
│                                                             │
│   并发调度              → useTransition, useDeferredValue    │
│                                                             │
│   表单/数据             → useActionState, useFormStatus,     │
│   (React 19)             useOptimistic, use()               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

核心原则:

  1. Hook 规则源于链表存储 —— 理解"为什么"比记住规则更重要
  2. state 是不可变的 —— 新对象 = 新渲染;函数式更新避免陈旧闭包
  3. Effect 同步外部系统 —— 不是 componentDidMount/Update/Unmount 的替代品
  4. 能不用 Effect 就不用 —— 计算值、事件处理、key 重置都比 Effect 更合适
  5. useMemo/useCallback 不是免费的 —— 先测量,再优化
  6. 并发模式用 useTransition —— 让 UI 在昂贵计算时保持响应
  7. 自定义 Hook 是逻辑复用 —— 不是状态共享;注意返回值引用稳定性

章节测试 ​

测试1:Hook 规则 ​

为什么 Hook 不能在条件语句中调用?请从内部实现角度解释。

测试2:useState ​

以下代码有什么问题?如何修复?

tsx
function Counter() {
  const [count, setCount] = useState(
    JSON.parse(localStorage.getItem('count') ?? '0')
  )
  // ...
}
1
2
3
4
5
6

测试3:useReducer ​

将以下 useState 代码改写为 useReducer,并定义判别联合的 Action 类型:

tsx
const [isLoading, setIsLoading] = useState(false)
const [data, setData] = useState<Data | null>(null)
const [error, setError] = useState<string | null>(null)
1
2
3

测试4:useRef ​

组件需要记住上一次 props.value 的值,并在变化时打印日志。请用 useRef 实现。

测试5:useEffect ​

以下 Effect 在 React StrictMode 下有什么问题?

tsx
useEffect(() => {
  const ws = new WebSocket('wss://example.com')
  ws.onmessage = (e) => setMessages(prev => [...prev, e.data])
}, [])
1
2
3
4

测试6:useMemo ​

以下场景加 useMemo 是否合理?为什么?

tsx
const doubled = useMemo(() => count * 2, [count])
1

测试7:并发 Hook ​

useTransition 和 useDeferredValue 的核心区别是什么?各自适用什么场景?

测试8:React 19 ​

use() 与 useContext() 相比有哪些关键区别?


参考答案 ​

测试1答案 ​

React 内部用单向链表存储每个组件实例的 Hook 状态,按调用顺序索引。如果 Hook 在条件/循环中调用,两次渲染间的调用顺序可能不同,链表节点无法正确对应,导致状态串位。这是由 React Fiber 架构中 Hook 的链表实现决定的。

测试2答案 ​

问题:localStorage.getItem() 在每次渲染时都会执行,尽管只有首次的值被使用。修复:使用惰性初始化函数 useState(() => JSON.parse(localStorage.getItem('count') ?? '0')),React 只在首次渲染时调用它。

测试3答案 ​

tsx
type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string }

type Action<T> =
  | { type: 'start_loading' }
  | { type: 'success'; data: T }
  | { type: 'error'; error: string }

function reducer<T>(state: RequestState<T>, action: Action<T>): RequestState<T> {
  switch (action.type) {
    case 'start_loading': return { status: 'loading' }
    case 'success':       return { status: 'success', data: action.data }
    case 'error':         return { status: 'error', error: action.error }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

测试4答案 ​

tsx
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>()
  useEffect(() => { ref.current = value })
  return ref.current
}
1
2
3
4
5

测试5答案 ​

StrictMode 下组件会挂载 → 卸载 → 重新挂载。空依赖数组的 Effect 会先建立连接、清理、再建立连接。如果清理函数没有关闭 WebSocket(当前代码缺少 return () => ws.close()),会导致连接泄漏。正确做法:添加 return () => ws.close()。

测试6答案 ​

不合理。count * 2 是 O(1) 的简单算术运算,useMemo 自身的开销(闭包创建、依赖比较)可能比直接计算还大。useMemo 只有在计算确实昂贵(大数组操作、复杂对象构建)时才值得使用。

测试7答案 ​

useTransition 返回 [isPending, startTransition],适合你有 setState 控制权的场景——用 startTransition 包裹自己的 setState 调用来标记非紧急更新。useDeferredValue 返回延迟后的值,适合props 来自外部、你无法控制更新时机的场景——传入一个外部值,React 在空闲时更新它。

测试8答案 ​

  1. use() 可以在条件语句和循环中调用(不依赖链表索引),useContext() 必须在顶层调用
  2. use() 可以读取 Promise(需要外层 Suspense),useContext() 只能读 Context

相关笔记 ​

  • [[01-react-components-and-rendering]] - React 组件模型和渲染机制
  • [[08-testing-performance-production]] - 测试与性能优化(含 useMemo/useCallback 性能分析)
  • [[05-nextjs-app-router-and-rendering]] - Next.js App Router(Server Components、Server Actions 与 React 19 Hook 的关系)

下一步学习 ​

  • [ ] 阅读 08 - 测试与性能优化
  • [ ] 阅读 05 - Next.js App Router
  • [ ] 阅读 React 官方文档 You Might Not Need an Effect
  • [ ] 阅读 React 19 发布博客中关于新 Hook 的说明

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇2. React 组件渲染、协调与数据流 / React Components, Rendering, and Data Flow
下一篇4. 路由、表单与组件架构 / Routing, Forms, and Component Architecture

持续记录,持续成长

Copyright © Tidenflow