Lesson 06:useReducer 重构 + 性能优化
🧩 本节信息卡(学习前先看)
- 阶段定位:Phase 1(基础篇)
- 推荐时长:60~90 分钟(首次学习)
- 先修要求:完成 Lesson 05,保留 storage.ts、编辑功能与组件接口
- 学习产出:用纯 reducer 管理任务,运行测试并理解记忆化边界
✅ 本节完成标准(自检清单)
- [ ] 我可以独立复现文中的核心代码片段
- [ ] 我能解释“为什么这样实现”,而不只是“照着写”
- [ ] 我记录了至少 1 个踩坑点和修复方法
🧭 本节统一学习流程
- 学习目标:先明确本节要解决的业务问题与核心 API。
- 主线实战:跟随课程实现可运行功能(先跑通,再优化)。
- 原理深挖:理解为什么这样设计,以及常见误区。
- 练习挑战:完成 L1/L2(阶段收官课建议加 L3)巩固迁移能力。
- 本节小结:回顾“做了什么 / 学到了什么 / 下节前检查项”。
建议节奏:阅读 20% + 编码 60% + 复盘 20%。
🎯 本节目标:用
useReducer统一管理 Todo 状态逻辑,学习React.memo/useMemo/useCallback性能优化。📦 本节产出:Phase 1 Todo App 完成版——逻辑清晰、性能优化、代码规范。
一、为什么需要 useReducer?
App.tsx 中的状态逻辑越来越分散:addTodo、toggleTodo、deleteTodo、editTodo、clearCompleted…… 每个都单独写一个函数操作同一个 todos state。
useReducer 把"做什么"和"怎么做"分离开:组件只负责 dispatch(做什么),reducer 负责具体逻辑(怎么做)。
二、useReducer 语法
const [state, dispatch] = useReducer(reducer, initialState)
// ↑ ↑ ↑ ↑
// 当前状态 发送动作 处理函数 初始值三、重构 Todo App
3.1 定义 Action 类型和 Reducer
// src/todoReducer.ts
import type { Todo } from './types'
// 所有可能的操作(用联合类型精确定义)
type TodoAction =
| { type: 'ADD'; text: string }
| { type: 'TOGGLE'; id: number }
| { type: 'DELETE'; id: number }
| { type: 'EDIT'; id: number; text: string }
| { type: 'CLEAR_COMPLETED' }
function todoReducer(state: Todo[], action: TodoAction): Todo[] {
switch (action.type) {
case 'ADD':
return [...state, { id: Math.max(0, ...state.map(todo => todo.id)) + 1, text: action.text, completed: false }]
case 'TOGGLE':
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
)
case 'DELETE':
return state.filter(todo => todo.id !== action.id)
case 'EDIT':
return state.map(todo =>
todo.id === action.id ? { ...todo, text: action.text } : todo
)
case 'CLEAR_COMPLETED':
return state.filter(todo => !todo.completed)
default:
return state
}
}
export { todoReducer }
export type { TodoAction }3.2 重构 App.tsx
// src/App.tsx
import { useReducer, useState, useEffect } from 'react'
import type { Filter } from './types'
import { loadTodos } from './storage'
import { todoReducer } from './todoReducer'
import Header from './components/Header'
import TodoInput from './components/TodoInput'
import TodoFilter from './components/TodoFilter'
import TodoList from './components/TodoList'
function App() {
// ✅ useReducer 替代 useState + 多个处理函数
const [todos, dispatch] = useReducer(todoReducer, [], loadTodos)
const [filter, setFilter] = useState<Filter>('all')
const [storageError, setStorageError] = useState('')
// 持久化:沿用 Lesson 05 对外部写入结果反馈的局部 lint 例外
/* eslint-disable react-hooks/set-state-in-effect -- 反馈外部存储写入结果,允许一次额外渲染 */
useEffect(() => {
try {
localStorage.setItem('todos', JSON.stringify(todos))
setStorageError('')
} catch {
setStorageError('本次修改未能保存,刷新页面可能丢失。')
}
}, [todos])
/* eslint-enable react-hooks/set-state-in-effect */
// 派生数据
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed
if (filter === 'completed') return todo.completed
return true
})
const completedCount = todos.filter(t => t.completed).length
const activeCount = todos.length - completedCount
return (
<div className="min-h-screen bg-linear-to-br from-indigo-50 via-white to-cyan-50">
<div className="max-w-xl mx-auto px-4 py-12">
<Header total={todos.length} completed={completedCount} />
{storageError && <p role="alert">{storageError}</p>}
{/* 注意:现在传 dispatch,不再传单独的函数 */}
<TodoInput onAdd={(text) => dispatch({ type: 'ADD', text })} />
<div className="flex items-center justify-between mb-4">
<TodoFilter current={filter} onChange={setFilter} />
{completedCount > 0 && (
<button
onClick={() => dispatch({ type: 'CLEAR_COMPLETED' })}
className="text-sm text-gray-400 hover:text-red-500 transition-colors"
>
清除已完成 ({completedCount})
</button>
)}
</div>
<TodoList
todos={filteredTodos}
onToggle={(id) => dispatch({ type: 'TOGGLE', id })}
onDelete={(id) => dispatch({ type: 'DELETE', id })}
onEdit={(id, text) => dispatch({ type: 'EDIT', id, text })}
/>
<p className="mt-6 text-center text-sm text-gray-400">
{activeCount} 个任务未完成
</p>
</div>
</div>
)
}
export default App3.3 useState vs useReducer 对比
| useState | useReducer | |
|---|---|---|
| 适合 | 简单、独立的状态 | 复杂、关联的状态逻辑 |
| 更新方式 | setState(newValue) | dispatch({ type, payload }) |
| 逻辑位置 | 分散在各个事件处理中 | 集中在 reducer 函数中 |
| 可测试性 | 需要渲染组件测试 | reducer 是纯函数,直接测试 |
| TypeScript | 类型推断足够 | 联合类型提供精确 action 约束 |
四、性能优化
4.1 什么时候需要优化?
IMPORTANT
先用 Profiler 或实际交互确定瓶颈,再决定是否记忆化。本课为了说明机制演示这些 API,并不表示这个小型 Todo 应用已经需要它们。
4.2 React.memo — 跳过不必要的重新渲染
import { memo } from 'react'
// 没有 memo:App 重新渲染时,TodoItem 全部重新渲染(即使 props 没变)
// 有 memo:Props 不变时通常可以跳过父组件导致的渲染;自身 state 或订阅的 Context 更新仍会渲染
const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete, onEdit }: TodoItemProps) {
console.log(`TodoItem ${todo.id} rendered`) // 观察渲染次数
// ...组件代码
})4.3 useCallback — 稳定回调函数引用
memo 有个陷阱:每次 App 渲染都会创建新的函数引用,导致 memo 失效!
// ❌ 每次 App 渲染,onToggle 都是新函数 → memo 对比 props 发现变了 → 白费
<TodoItem onToggle={(id) => dispatch({ type: 'TOGGLE', id })} />
// ✅ useCallback 缓存函数引用
import { useCallback } from 'react'
const handleToggle = useCallback((id: number) => {
dispatch({ type: 'TOGGLE', id })
}, [dispatch]) // dispatch 是稳定的,不会变
const handleDelete = useCallback((id: number) => {
dispatch({ type: 'DELETE', id })
}, [dispatch])
const handleEdit = useCallback((id: number, text: string) => {
dispatch({ type: 'EDIT', id, text })
}, [dispatch])
// 替换 App 中的 TodoList;TodoList 再原样传给各个 TodoItem
<TodoList todos={filteredTodos} onToggle={handleToggle} onDelete={handleDelete} onEdit={handleEdit} />4.4 useMemo — 缓存昂贵计算
import { useMemo } from 'react'
// 如果 todos 很多(如 10000 条),每次渲染都过滤一遍很昂贵
const filteredTodos = useMemo(() => {
return todos.filter(todo => {
if (filter === 'active') return !todo.completed
if (filter === 'completed') return todo.completed
return true
})
}, [todos, filter]) // 只在 todos 或 filter 变化时重新计算4.5 三者关系
五、🧪 初尝测试:Reducer 是最好的起点
还记得 todoReducer 是一个纯函数吗?输入确定 → 输出确定,没有副作用。 这里可以直接传入 state 和 action,检查返回结果,不必启动浏览器。Phase 3 会系统介绍 Vitest,本节先为已有实现补上测试:
npm install -D vitest@4// src/todoReducer.test.ts
import { describe, it, expect } from 'vitest'
import { todoReducer } from './todoReducer'
describe('todoReducer', () => {
it('ADD 应该新增一个未完成任务', () => {
const result = todoReducer([], { type: 'ADD', text: '学 React' })
expect(result).toHaveLength(1)
expect(result[0].text).toBe('学 React')
expect(result[0].completed).toBe(false)
})
it('TOGGLE 应该切换指定任务的完成状态', () => {
const initial = [{ id: 1, text: '测试', completed: false }]
const result = todoReducer(initial, { type: 'TOGGLE', id: 1 })
expect(result[0].completed).toBe(true)
})
it('DELETE 应该移除指定任务', () => {
const initial = [
{ id: 1, text: 'A', completed: false },
{ id: 2, text: 'B', completed: false },
]
const result = todoReducer(initial, { type: 'DELETE', id: 1 })
expect(result).toHaveLength(1)
expect(result[0].id).toBe(2)
})
it('CLEAR_COMPLETED 应该移除所有已完成任务', () => {
const initial = [
{ id: 1, text: 'A', completed: true },
{ id: 2, text: 'B', completed: false },
]
const result = todoReducer(initial, { type: 'CLEAR_COMPLETED' })
expect(result).toHaveLength(1)
expect(result[0].completed).toBe(false)
})
})在 package.json 现有的 scripts 对象中加入 "test": "vitest",保留 dev、build 和 lint。也可以运行:
npm pkg set scripts.test="vitest"npm test这 4 个测试应当通过。Reducer 集中逻辑后便于独立测试;useState 的复杂计算同样可以抽成纯函数,不能据此断言 useReducer 总是更好。一次性验证可运行 npm test -- --run。
TIP
Phase 3 的 Lesson 25 会系统教 Vitest + Testing Library。但在这里先让你体验一下:测试不是"做完之后的流水作业",而是开发过程中的质量保障。 很多优秀的团队采用 TDD(测试驱动开发),先写测试再写实现。
六、🧠 深度专题:React Compiler
6.1 手动优化的痛点
// 开发者需要手动决定:哪里加 memo?哪里加 useCallback?依赖数组写对了吗?
// 这是心智负担,也容易出错。6.2 React Compiler(原名 React Forget)
React Compiler 1.0 已于 2025 年 10 月稳定发布。它在构建时分析组件和 Hook,为可安全优化的值与 UI 添加记忆化,减少手动使用这些 API 的需求。参见 React Compiler 1.0 公告。
- 安装 React 19 不会自动启用 Compiler,还需要构建工具集成。
- Compiler 不会消除网络延迟、长列表 DOM 或所有昂贵计算;仍需测量实际效果。
- 本课程的 Vite 模板不启用 Compiler,便于观察手动记忆化;Lesson 30 再演示配置。
理解 memo、useCallback 和 useMemo 的引用与依赖关系,仍然有助于阅读已有代码和排查性能问题。
七、React 19 新增:useActionState
React 19 引入了 useActionState,适用于表单操作:
import { useActionState } from 'react'
function TodoForm({ onAdd }: { onAdd: (text: string) => void }) {
const [error, submitAction, isPending] = useActionState(
async (_prevState: string | null, formData: FormData) => {
const text = String(formData.get('todo') ?? '').trim()
if (text.length < 2) return '任务至少 2 个字符'
onAdd(text) // 本地添加;服务端操作在 Phase 3 中实现
return null // 无错误
},
null // 初始错误状态
)
return (
<form action={submitAction}>
<input name="todo" aria-label="新任务" />
<button disabled={isPending}>
{isPending ? '提交中...' : '添加'}
</button>
{error && <p className="text-red-500">{error}</p>}
</form>
)
}这是替换
TodoInput的可选示例,需要传入onAdd。isPending在实际异步提交时更有用;Phase 3 再结合 Server Actions 使用。
八、Phase 1 完成!🎉
Phase 1 全面回顾
| 你掌握了 | 关键概念 |
|---|---|
| 项目搭建 | Vite + React 19 + TypeScript + Tailwind v4 |
| 组件设计 | 函数组件、Props、children、组合模式 |
| 状态管理 | useState、useReducer、派生数据 |
| 副作用 | useEffect、依赖数组、清理函数 |
| DOM 操作 | useRef |
| 性能优化 | memo、useMemo、useCallback |
| 核心原理 | Virtual DOM、Reconciliation、Fiber、闭包陷阱 |
| React 18/19 | React 18 引入的自动批处理,以及 React 19 的 use()、useActionState |
九、练习
- 测试 reducer:单独测试
todoReducer,验证每个 action 的行为 - 添加 undo:保存操作历史,实现撤销功能(提示:用 state 记录之前的 todos 快照)
- DevTools Profiler:安装 React DevTools,用 Profiler 观察 memo 前后的渲染差异
十、📌 本节小结
| 你做了什么 | 你学到了什么 |
|---|---|
用 useReducer 重构 Todo 状态逻辑 | 把复杂状态更新集中到 reducer,降低维护成本 |
为组件添加 React.memo / useMemo / useCallback | 先定位瓶颈,再做有证据的性能优化 |
了解 React 19 的 useActionState | 表单场景下把提交状态和错误状态集中管理 |
| 完成 Phase 1 收官 | 具备进入多页面工程(Phase 2)的能力 |
进入下一课前检查清单:
- [ ] 能解释
useState与useReducer的取舍边界 - [ ] 能写出纯函数 reducer 并为 action 做类型约束
- [ ] 知道何时应该/不应该使用
memo与useCallback