0%

已完成 0/29 关

教学模式

你可以随时调整。

此设置调整教师的教学方式,无需重新生成课程。

第 21 关 含代码 约 10 分钟

选择 State 结构:分组、避免矛盾与消除冗余

选择 State 结构:分组、避免矛盾与消除冗余

编写一个能够正常运行的组件并不难,但编写一个在日后需求变更时不容易滋生 bug、且易于维护重构的组件,很大程度上取决于你如何组织你的 State 结构。

一个设计糟糕的 State 结构就像一团乱麻,容易出现状态不同步、相互矛盾和难以复现的奇怪缺陷。官方为我们总结了设计 State 结构的 五大黄金原则。


如果两个状态变量总是同时发生改变,那么把它们合并成一个单独的对象状态,通常是明智的选择:

// ❌ 分散的写法
const [x, setX] = useState(0);
const [y, setY] = useState(0);

// ✅ 更好的做法:当二者总是绑定改变时,组合成单个对象
const [position, setPosition] = useState({ x: 0, y: 0 });

原则二:避免矛盾的 State(Avoid contradictions in state)

看看下面这段发送消息的表单代码:

// ❌ 极易滋生矛盾的危险写法!
const [text, setText] = useState('');
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);

如果开发人员在某个异步分支中写漏了一句 setIsSending(false),界面上就会同时出现 isSending === true 和 isSent === true 的怪象(“既在发送中,又已经发送完成”)。

正确方案:用枚举状态机替代多个布尔值

// ✅ 不可能出现矛盾的优雅写法
const [text, setText] = useState('');
const [status, setStatus] = useState('typing'); // 只能是 'typing' | 'sending' | 'sent'

在任何给定时刻,status 只能是这三个字符串中的一个,从根本上杜绝了非法状态并存的可能!


原则三:避免冗余的 State(Avoid redundant state)

这是绝大多数初学者最容易犯的错误:把能够直接计算得出的数据,也顺手存成了 State!

经典反例:全名(fullName)

// ❌ 存在严重冗余的写法
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState(''); // 冗余!

function handleFirstNameChange(e) {
  setFirstName(e.target.value);
  setFullName(e.target.value + ' ' + lastName); // 必须小心翼翼同步更新,极易漏改出 bug
}

黄金重构准则:

如果一个值能够在组件渲染期间,从当前的 props 或已有的其他 state 变量中直接计算出来,那么它绝对不应该被存为 State!

重构后的极致纯净代码:

// ✅ 正确做法:只存原始源头,其余在渲染时即时计算
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');

// fullName 是纯计算变量,绝不放进 state!
const fullName = firstName + ' ' + lastName;

每次组件因 firstName 或 lastName 改变而重新渲染时,fullName 都会自动以光速重新计算出最新结果,代码减少了一半,而且 100% 不可能出现数据脱节的 bug!


原则四:避免重复的 State(Avoid duplication in state)

当你在一个列表和当前选中的项之间建立关联时:

// ❌ 危险的重复存储!
const [items, setItems] = useState([
  { id: 0, title: '地瓜' },
  { id: 1, title: '土豆' }
]);
const [selectedItem, setSelectedItem] = useState(items[0]); // 拷贝了完整对象!

如果用户稍后在列表中修改了地瓜的名字(比如改成“烤地瓜”),selectedItem 里的名字依然是旧的“地瓜”!

正确方案:只记录选中项的 ID

// ✅ 只记录轻量标识符
const [items, setItems] = useState([
  { id: 0, title: '地瓜' },
  { id: 1, title: '土豆' }
]);
const [selectedId, setSelectedId] = useState(0);

// 在渲染时实时查表匹配对象:
const selectedItem = items.find(item => item.id === selectedId);

无论列表怎么被修改,通过 selectedId 找到的永远是列表里的同一真实数据,彻底消除了数据分叉!


原则五:避免深度嵌套的 State(Avoid deeply nested state)

如果你的状态是一个包含很多层级树状嵌套的庞大对象(比如一个多层评论回复树或包含十多层文件夹的目录),更新其中深层的一项需要层层展开拷贝,代码会极其臃肿。 推荐将深度嵌套的数据扁平化(Flat / Normalized),就像关系型数据库一样,通过对象字典和 ID 相互关联。


总结

  • 合并相关:总是同步改变的属性放在同一个对象中;
  • 杜绝矛盾:使用单个 status 状态替代多个可能冲突的布尔标记;
  • 消灭冗余:能在渲染计算中顺手算出来的东西,坚决不要存成 state;
  • 避免重复:列表选中态存 ID,而不是拷贝一整份对象;
  • 扁平归一:避免深层嵌套以保持更新代码简洁健壮。
下一站:在组件间共享状态:状态提升与受控组件 →
© 2026 React 官方教程