选择状态结构
良好的状态结构可以区分出一个易于修改和调试的组件与一个不断产生错误的组件。以下是设计状态结构时应考虑的一些技巧。
您将学习
- 何时使用单个状态变量与多个状态变量
- 组织状态时应避免什么
- 如何修复状态结构的常见问题
构建状态的原则
当你编写一个包含某些状态的组件时,你必须决定使用多少个状态变量以及它们的数据结构应该是什么。即使使用次优的状态结构也有可能编写出正确的程序,但有一些原则可以指导你做出更好的选择:
- 将相关状态分组。如果你总是同时更新两个或更多状态变量,请考虑将它们合并为单个状态变量。
- 避免状态矛盾。当状态的结构方式可能导致多个状态片段相互矛盾或“不一致”时,你就为错误留下了空间。尽量避免这种情况。
- 避免冗余状态。如果你可以在渲染期间从组件的 props 或其现有状态变量计算出某些信息,则不应将该信息放入该组件的状态中。
- 避免状态重复。当相同的数据在多个状态变量之间或嵌套对象内部重复时,很难保持它们同步。尽可能减少重复。
- 避免深度嵌套的状态。深度层次化的状态更新起来不太方便。在可能的情况下,倾向于以扁平化的方式构建状态。
这些原则背后的目标是使状态易于更新而不引入错误。从状态中移除冗余和重复的数据有助于确保其所有部分保持同步。这类似于数据库工程师可能希望“规范化”数据库结构以减少错误的机会。引用阿尔伯特·爱因斯坦的话来说,“让你的状态尽可能简单——但不要过于简单。”
现在让我们看看这些原则如何在实际中应用。
将相关状态分组
有时你可能不确定是使用单个状态变量还是多个状态变量。
你应该这样做吗?
还是这个?
从技术上讲,这两种方法你都可以使用。但是如果两个状态变量总是一起变化,将它们合并成一个状态变量可能是个好主意。这样你就不会忘记总是让它们保持同步,就像这个例子中移动光标会同时更新红点的两个坐标:
另一种将数据分组到对象或数组中的情况是,当你不知道需要多少状态片段时。例如,当用户可以在表单中添加自定义字段时,这样做会很有帮助。
陷阱
如果你的状态变量是一个对象,请记住,你无法只更新其中的一个字段而不显式复制其他字段。例如,在上面的例子中,你不能执行setPosition({ x: 100 }),因为它会完全丢失y属性!相反,如果你只想设置x,你可以执行setPosition({ ...position, x: 100 }),或者将它们拆分成两个状态变量并执行setX(100)。
避免状态中的矛盾
这是一个带有isSending和isSent状态变量的酒店反馈表单:
虽然这段代码可以工作,但它为“不可能”的状态留下了可能性。例如,如果你忘记同时调用setIsSent 和 setIsSending,你可能会陷入 isSending 和 isSent同时为true 的情况。你的组件越复杂,就越难理解发生了什么。
由于isSending 和 isSent不应该同时为true,更好的做法是用一个 status状态变量来替代它们,该变量可以取三种有效状态之一:'typing'(初始)、'sending' 和 'sent':
你仍然可以声明一些常量以提高可读性:
但它们不是状态变量,所以你不用担心它们彼此不同步。
避免冗余状态
如果你可以在渲染期间从组件的 props 或其现有的状态变量计算出某些信息,你不应该将该信息放入该组件的状态中。
例如,看看这个表单。它可以工作,但你能找出其中任何冗余的状态吗?
这个表单有三个状态变量:firstName、lastName 和 fullName。然而,fullName是冗余的。你总是可以在渲染期间从fullName 和 firstName计算出lastName,因此将其从状态中移除。
你可以这样做:
在这里,fullName 不是一个状态变量。相反,它是在渲染期间计算的:
因此,变更处理程序不需要做任何特殊的事情来更新
避免状态中的重复
这个菜单列表组件允许您从几种旅行零食中选择一种:
目前,它将选中的项目作为一个对象存储在selectedItem状态变量中。然而,这并不理想: selectedItem 的内容与 items列表中的某个项目是同一个对象。 这意味着项目本身的信息在两个地方重复了。
为什么这是个问题?让我们使每个项目都可编辑:
请注意,如果你先点击某个项目的“选择”按钮,然后再编辑它,输入框会更新,但底部的标签却不会反映这些编辑。这是因为你复制了状态,并且忘记更新 selectedItem。
虽然你也可以更新selectedItem,但一个更简单的修复方法是消除重复。在这个例子中,与其使用一个selectedItem 对象(这会与 items内部的对象产生重复),不如在状态中保存selectedId,然后通过在该items数组中搜索具有该 ID 的项目来获取selectedItem:
之前的状态是这样重复的:
items = [{ id: 0, title: 'pretzels'}, ...]selectedItem = {id: 0, title: 'pretzels'}
但修改后变成了这样:
items = [{ id: 0, title: 'pretzels'}, ...]selectedId = 0
重复消失了,你只保留了必要的状态!
现在如果你编辑选中的项目,下方的消息会立即更新。这是因为setItems会触发重新渲染,而items.find(...)会找到更新了标题的项目。你不需要将选中的项目保存在状态中,因为只有选中的 ID是必要的。其余部分可以在渲染时计算。
避免深度嵌套的状态
想象一个由行星、大洲和国家组成的旅行计划。你可能会想使用嵌套的对象和数组来构建其状态,就像这个例子中一样:
现在假设你想添加一个按钮来删除一个你已经访问过的地方。你会怎么做?更新嵌套状态涉及从发生变化的部分开始,一路向上复制对象。删除一个深度嵌套的地点将涉及复制其整个父地点链。这样的代码可能会非常冗长。
如果状态嵌套过深导致难以更新,可以考虑将其“扁平化”。以下是重构此数据的一种方法。与其采用树状结构,让每个place都包含一个其子地点的数组,不如让每个地点持有一个其子地点ID的数组。然后存储一个从每个地点ID到对应地点的映射。
这种数据重构可能会让你联想到数据库表:
现在状态已经是“扁平化”(也称为“规范化”)的,更新嵌套项就变得更容易了。
现在要删除一个地点,你只需要更新两个层级的状态:
- 其父级地点的更新版本应从其
childIds数组中排除被移除的ID。 - 根“表”对象的更新版本应包含父级地点的更新版本。
以下是一个如何实现此操作的示例:
你可以根据需要任意嵌套状态,但将其“扁平化”可以解决许多问题。它使状态更容易更新,并有助于确保嵌套对象的不同部分不会出现重复。
有时,你也可以通过将一些嵌套状态移动到子组件中来减少状态嵌套。这对于不需要存储的临时 UI 状态(例如某个项目是否被悬停)非常有效。
回顾
- 如果两个状态变量总是一起更新,考虑将它们合并为一个。
- 仔细选择你的状态变量,以避免创建“不可能”的状态。
- 以能减少更新时出错可能性的方式来组织你的状态。
- 避免冗余和重复的状态,这样你就不需要保持它们同步。
- 不要将 props放入 状态中,除非你特意想要阻止更新。
- 对于像选择这样的 UI 模式,在状态中保存 ID 或索引,而不是对象本身。
- 如果更新深层嵌套状态很复杂,尝试将其扁平化。
Try out some challenges
Challenge 1 of 4:Fix a component that’s not updating #
This Clock component receives two props: color and time. When you select a different color in the select box, the Clock component receives a different color prop from its parent component. However, for some reason, the displayed color doesn’t update. Why? Fix the problem.
