
React 学习笔记:状态管理
用 State 响应输入
核心思想
React 采用声明式 UI 编程方式,开发者只需声明组件可以处于的不同状态,React 会自动计算如何更新 UI。
声明式 vs 命令式
| 命令式 | 声明式 |
|---|---|
| 直接操作 DOM 元素 | 声明期望的 UI 状态 |
| 手动控制每个 UI 变化 | 框架自动处理更新 |
| 如同手把手告诉司机怎么走 | 如同告诉司机目的地 |
实现声明式状态管理的 5 个步骤
步骤 1:定位视图状态
列出组件所有可能的 UI 状态:
- 无数据(按钮禁用)
- 输入中(按钮可用)
- 提交中(全部禁用,显示加载)
- 成功(显示成功信息)
- 错误(显示错误信息)
步骤 2:确定触发状态改变的输入
两种输入类型:
- 人为输入:点击、输入、导航等 → 事件处理函数
- 计算机输入:网络请求、定时器、图片加载等
步骤 3:用 useState 表示状态
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', 'success'
步骤 4:删除不必要的 state 变量
检查原则:
- 是否会导致矛盾?(如 isTyping 和 isSubmitting 不能同时为 true)
- 信息是否已存在于其他 state?
- 能否通过其他 state 推导得出?
步骤 5:连接事件处理函数
async function handleSubmit(e) {
e.preventDefault();
setStatus('submitting');
try {
await submitForm(answer);
setStatus('success');
} catch (err) {
setStatus('typing');
setError(err);
}
}
关键原则
- state 每部分都是"变化中的",让变化部分尽可能少
- 避免 state 重复,用更少的变量表示
- 确保内存中的 state 代表有效的 UI 状态
选择 State 结构
5 大构建原则
1. 合并关联的 state
总是同时更新的 state 变量应合并为一个:
// ❌ 分开管理,需要同步更新
const [x, setX] = useState(0);
const [y, setY] = useState(0);
// ✅ 合并为一个对象
const [position, setPosition] = useState({ x: 0, y: 0 });
更新对象 state 时必须展开所有字段:setPosition({ ...position, x: 100 })
2. 避免矛盾的 state
多个布尔值可能产生无效组合,用枚举值替代:
// ❌ isSending 和 isSent 可能同时为 true
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);
// ✅ 用 status 枚举
const [status, setStatus] = useState('typing'); // 'typing' | 'sending' | 'sent'
3. 避免冗余的 state
能从 props 或现有 state 计算出的值,不要存为 state:
// ❌ fullName 是冗余的
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
// ✅ 直接计算
const fullName = firstName + ' ' + lastName;
4. 避免重复的 state
同一数据不要在多处存储,只存 ID 而非对象:
// ❌ selectedItem 与 items 中的数据重复
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(items[0]);
// ✅ 只存 ID,通过查找获取
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(0);
const selectedItem = items.find(item => item.id === selectedId);
5. 避免深度嵌套的 state
嵌套过深时,将数据"扁平化":
// ❌ 树状嵌套,更新困难
{ id: 1, childPlaces: [{ id: 2, childPlaces: [...] }] }
// ✅ 扁平化,只存子节点 ID
{
1: { id: 1, childIds: [2, 3] },
2: { id: 2, childIds: [] }
}
核心要点
| 原则 | 目的 |
|---|---|
| 合并关联 state | 避免忘记同步更新 |
| 避免矛盾 state | 消除无效状态组合 |
| 避免冗余 state | 出错机会更少 |
| 避免重复 state | 保持数据一致性 |
| 扁平化嵌套 state | 简化更新操作 |
在组件间共享状态
核心概念:状态提升
当多个组件需要同步状态时,将 state 移到它们的公共父组件,再通过 props 向下传递。
状态提升前: 状态提升后:
Parent Parent (state 在这里)
/ \ / \
Child1 Child2 Child1 Child2
(state) (state) (props) (props)
状态提升 3 步法
步骤 1:从子组件中移除 state
// ❌ 子组件自己管理状态
function Panel() {
const [isActive, setIsActive] = useState(false);
// ...
}
// ✅ 从 props 接收
function Panel({ isActive, onShow }) {
// ...
}
步骤 2:从公共父组件传递数据
function Accordion() {
const [activeIndex, setActiveIndex] = useState(0);
return (
<>
<Panel
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
面板 1
</Panel>
<Panel
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
面板 2
</Panel>
</>
);
}
步骤 3:向下传递事件处理函数
子组件通过调用父组件传入的函数来"提升"状态:
function Panel({ title, children, isActive, onShow }) {
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={onShow}>显示</button>
)}
</section>
);
}
受控 vs 非受控组件
| 类型 | 定义 | 状态来源 |
|---|---|---|
| 受控组件 | 由 props 驱动 | 父组件 state |
| 非受控组件 | 由自身 state 驱动 | 组件内部 |
单一数据源原则
每个状态应该只存在于一个指定组件中:
- 需要同步的状态 → 提升到公共父级
- 不同的状态 → 传递给需要它的子级
- 不要在多个组件中复制相同的状态
核心要点
- 状态提升的本质:将"谁拥有这个状态"的问题向上移动
- 事件处理函数向下传递,让子组件能"回调"修改父组件状态
- 同一数据只存一份,避免同步问题
对 state 进行保留和重置
核心原理
React 通过组件在渲染树中的位置来跟踪 state,而非 JSX 标签本身。
State 保留规则
| 场景 | 结果 |
|---|---|
| 相同位置 + 相同组件类型 | ✅ 保留 state |
| 相同位置 + 不同组件 | ❌ 重置 state |
| 不同位置 + 相同组件 | ❌ 各自独立 |
保留 state 的条件
// ✅ 保留:始终在相同位置渲染 Counter
<div>
{isFancy ? (
<Counter isFancy={true} />
) : (
<Counter isFancy={false} />
)}
</div>
React 看到的是:div > 第一个子组件 > Counter,位置没变,state 保留。
重置 state 的场景
1. 相同位置渲染不同组件
// ❌ Counter 被 p 替换,state 重置
<div>
{isPaused ? (
<p>待会见!</p> // 不同组件
) : (
<Counter /> // 原组件
)}
</div>
2. 父容器类型改变
// ❌ section → div,整棵子树重置
{isFancy ? (
<div><Counter isFancy={true} /></div>
) : (
<section><Counter isFancy={false} /></section>
)}
使用 key 强制重置
当需要在相同位置切换组件并重置 state 时,使用 key:
// ❌ state 被保留(React 认为是同一个 Counter)
{isPlayerA ? (
<Counter person="Taylor" />
) : (
<Counter person="Sarah" />
)}
// ✅ state 被重置(key 不同,React 认为是不同组件)
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
典型应用:重置表单
// 切换联系人时清空输入框
<Chat key={to.id} contact={to} />
常见陷阱
不要嵌套组件定义
// ❌ 每次渲染创建新组件,state 不断重置
function Parent() {
function Child() { // 每次渲染都是不同的函数
const [text, setText] = useState('');
return <input value={text} onChange={e => setText(e.target.value)} />;
}
return <Child />;
}
// ✅ 组件定义在顶层
function Child() {
const [text, setText] = useState('');
return <input value={text} onChange={e => setText(e.target.value)} />;
}
function Parent() {
return <Child />;
}
核心要点
| 规则 | 说明 |
|---|---|
| 位置决定 state | React 按树形结构位置匹配组件 |
| key 重置 state | 不同 key = 不同组件 = 新 state |
| 避免嵌套定义 | 否则每次渲染都创建新组件 |
| 树结构匹配 | 渲染树结构变化会导致 state 重置 |
迁移状态逻辑至 Reducer 中
为什么使用 Reducer
当组件状态更新逻辑复杂(多个事件处理程序以相似方式修改 state)时,使用 reducer 可以:
- 将所有状态更新逻辑集中到一个地方
- 让事件处理程序只负责"描述发生了什么"
- 让 reducer 负责"决定如何更新状态"
useState → useReducer 迁移 3 步法
步骤 1:将 setState 改为 dispatch action
// ❌ 直接设置状态
function handleAddTask(text) {
setTasks([...tasks, { id: nextId++, text, done: false }]);
}
// ✅ dispatch action
function handleAddTask(text) {
dispatch({ type: 'added', id: nextId++, text });
}
步骤 2:编写 reducer 函数
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added':
return [...tasks, { id: action.id, text: action.text, done: false }];
case 'changed':
return tasks.map(t => t.id === action.task.id ? action.task : t);
case 'deleted':
return tasks.filter(t => t.id !== action.id);
default:
throw Error('未知 action: ' + action.type);
}
}
步骤 3:使用 useReducer 替换 useState
// ❌ useState
const [tasks, setTasks] = useState(initialTasks);
// ✅ useReducer
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
Reducer 编写原则
- 必须是纯函数 — 不要有异步请求、定时器或副作用
- 每个 action 描述单一用户交互 — 如
reset_form而非 5 个set_field - 不可变更新 — 返回新对象/数组,不直接修改原 state
useState vs useReducer 对比
| 维度 | useState | useReducer |
|---|---|---|
| 代码量 | 初始少 | 初始多,复杂时少 |
| 可读性 | 简单逻辑清晰 | 复杂逻辑更清晰 |
| 可调试性 | 难追踪 | 可打印 action 日志 |
| 可测试性 | 依赖组件 | 纯函数,可独立测试 |
| 适用场景 | 简单状态 | 复杂状态逻辑 |
useReducer API 详解
const [state, dispatch] = useReducer(reducer, initialArg, init?)
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
| reducer | Function | (state, action) => newState |
| initialArg | any | 初始状态值 |
| init | Function(可选) | 惰性初始化函数:init(initialArg) → initialState |
返回值:
| 返回 | 类型 | 说明 |
|---|---|---|
| state | any | 当前状态值 |
| dispatch | Function | (action) => void,派发 action 更新状态 |
dispatch 的 action 参数:
// action 是任意类型,惯例为带 type 属性的对象
dispatch({ type: 'incremented_age' });
dispatch({ type: 'changed_name', nextName: 'Bob' });
init惰性初始化:
// 适合初始状态需要复杂计算时
function init(initialCount) {
return { count: initialCount, timestamp: Date.now() };
}
const [state, dispatch] = useReducer(reducer, initialCount, init);
使用场景对比
// useState 适合:简单独立的状态
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// useReducer 适合:多个相关状态 + 复杂更新逻辑
const [state, dispatch] = useReducer(reducer, {
name: 'Taylor',
age: 42,
tasks: []
});
核心要点
| 要点 | 说明 |
|---|---|
| action 描述事件 | dispatch 只说"发生了什么" |
| reducer 决定更新 | 根据 action 类型计算新状态 |
| 纯函数 | reducer 不能有副作用 |
| 惰性初始化 | 第三个参数可延迟计算初始状态 |
使用 Context 深层传递参数
问题:Prop 逐级透传
当数据需要经过多层中间组件传递时,会导致代码冗长:
// ❌ 需要手动传递每一层
<Layout posts={posts}>
<Sidebar posts={posts}>
<Profile posts={posts}>
<BlogPosts posts={posts} /> // 终于用到了
Context 解决方案
Context 允许父组件直接向任意深度的子组件提供数据,无需逐层传递。
使用 Context 3 步法
步骤 1:创建 Context
// LevelContext.js
import { createContext } from 'react';
export const LevelContext = createContext(1); // 默认值
步骤 2:使用 Context(子组件)
// Heading.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext';
export default function Heading({ children }) {
const level = useContext(LevelContext); // 读取最近的 Provider 值
return <h1>{children}</h1>;
}
步骤 3:提供 Context(父组件)
// Section.js
import { LevelContext } from './LevelContext';
export default function Section({ level, children }) {
return (
<section>
<LevelContext value={level}> {/* 提供值给子组件 */}
{children}
</LevelContext>
</section>
);
}
Context 继承特性
类似 CSS 属性继承,Context 值会向下穿透,除非被中间 Provider 覆盖:
<LevelContext value={1}> {/* 提供 level=1 */}
<Section>
<Heading /> {/* useContext → 1 */}
<LevelContext value={2}> {/* 覆盖为 level=2 */}
<Heading /> {/* useContext → 2 */}
</LevelContext>
</Section>
</LevelContext>
同一组件使用并提供 Context
Section 可以读取上层 Context 并提供递增的值:
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section>
<LevelContext value={level + 1}> {/* 自动递增 */}
{children}
</LevelContext>
</section>
);
}
Context vs Props 对比
| 维度 | Props | Context |
|---|---|---|
| 数据流向 | 父 → 子(显式) | 祖先 → 后代(隐式) |
| 中间组件 | 需要传递 | 自动穿透 |
| 可读性 | 清晰明了 | 需查看 Provider |
| 耦合度 | 低 | 相对高 |
使用 Context 前的替代方案
- 先尝试传递 props — 数据流更清晰
- 抽象组件 + children — 减少中间层
- 最后再考虑 Context
Context 常见使用场景
| 场景 | 示例 |
|---|---|
| 主题切换 | 暗色/亮色模式 |
| 当前用户 | 登录状态、用户信息 |
| 路由 | 当前路由、导航状态 |
| 表单 | 多个表单共享验证状态 |
核心要点
| 要点 | 说明 |
|---|---|
| 创建 | createContext(defaultValue) |
| 使用 | useContext(MyContext) 读取值 |
| 提供 | <MyContext value={...}> 包裹子树 |
| 穿透 | 自动跳过中间组件,直达使用处 |
| 覆盖 | 中间 Provider 可提供不同值 |
状态管理选型决策树
单个组件内部状态 → useState
多个组件共享(兄弟) → 状态提升
复杂状态更新逻辑 → useReducer
深层传递(祖孙) → Context
全局/跨模块状态 → Context + useReducer,或 Zustand/Redux