保留与重置状态
状态在组件之间是隔离的。React 会根据组件在 UI 树中的位置来跟踪哪个状态属于哪个组件。你可以控制在重新渲染时何时保留状态以及何时重置它。
你将学习
- React 何时选择保留或重置状态
- 如何强制 React 重置组件的状态
- 键(key)和类型如何影响状态是否被保留
状态与渲染树中的位置相关联
React 会为你的 UI 中的组件结构构建渲染树。
当你给组件添加状态时,你可能会认为状态“存在于”组件内部。但实际上状态是由 React 持有的。React 通过组件在渲染树中的位置,将其持有的每一块状态与正确的组件关联起来。
这里只有一个<Counter />JSX 标签,但它在两个不同的位置被渲染:
它们在树中的结构如下:


React 树
这是两个独立的计数器,因为每个都在树中的各自位置上被渲染。在使用 React 时,你通常不需要考虑这些位置,但了解其工作原理可能很有用。
在 React 中,屏幕上的每个组件都有完全隔离的状态。例如,如果你并排渲染两个Counter组件,它们各自都会获得自己独立的score和hover状态。
尝试点击两个计数器,注意它们互不影响:
如你所见,当一个计数器被更新时,只有该组件的状态会被更新:


更新状态
只要你在树中的同一位置渲染相同的组件,React 就会保持其状态。要验证这一点,请先递增两个计数器,然后通过取消勾选“渲染第二个计数器”复选框来移除第二个组件,接着再勾选它以重新添加回来:
请注意,当你停止渲染第二个计数器的那一刻,它的状态就完全消失了。这是因为当 React 移除一个组件时,它会销毁其状态。


删除组件
当你勾选“渲染第二个计数器”时,第二个Counter 及其状态会从零开始初始化(score = 0)并添加到 DOM 中。


添加组件
只要组件在 UI 树中的位置被渲染,React 就会保留其状态。如果它被移除,或者在相同位置渲染了不同的组件,React 会丢弃其状态。
相同位置下的相同组件会保留状态
在这个例子中,有两个不同的<Counter />标签:
当你勾选或清除复选框时,计数器状态不会重置。无论 isFancy 是 true还是false,你始终有一个<Counter /> 作为根 div 返回的 App组件的第一个子组件:


更新 App 的状态不会重置 Counter,因为Counter 保持在相同的位置
它是相同位置上的相同组件,因此从 React 的角度来看,它是同一个计数器。
陷阱
请记住,对 React 来说重要的是 UI 树中的位置,而不是 JSX 标记中的位置! 这个组件有两个 return 语句,其中包含 <Counter /> JSX 标签分别在 if条件内外:
你可能以为勾选复选框时状态会重置,但并没有!这是因为 这两个 <Counter />标签都在同一个位置渲染。React 不知道你在函数中放置条件的位置。它“看到”的只是你返回的树。
在这两种情况下,App组件都返回一个<div>,其第一个子元素是<Counter />。对 React 来说,这两个计数器具有相同的“地址”:根的第一个子元素的第一个子元素。这就是 React 在前后渲染之间匹配它们的方式,无论你如何组织逻辑。
同一位置的不同组件会重置状态
在这个例子中,勾选复选框会将 <Counter>替换为一个<p>:
你在同一位置切换不同的组件类型。最初,<div>的第一个子元素包含一个Counter。但当你换入一个p时,React 从 UI 树中移除了Counter并销毁了它的状态。


当Counter变为p时,Counter被删除,p被添加


当切换回来时,p被删除,Counter被添加
此外,当你在同一位置渲染不同的组件时,它会重置其整个子树的状态。要了解其工作原理,请递增计数器然后勾选复选框:
当你点击复选框时,计数器状态会被重置。虽然你渲染了一个Counter,但div的第一个子元素从section变为div。当子元素section从 DOM 中移除时,其下方的整个树(包括Counter及其状态)也随之被销毁。


当section变为div时,section被删除,新的div被添加


当切换回来时,div被删除,新的section被添加
根据经验法则,如果你想在重新渲染之间保留状态,你的树结构需要在两次渲染之间“匹配”。如果结构不同,状态就会被销毁,因为当React从树中移除一个组件时,它会销毁该组件的状态。
陷阱
这就是为什么你不应该嵌套组件函数定义。
在这里,MyTextField组件函数定义在MyComponent内部:
每次点击按钮时,输入状态都会消失!这是因为每次不同的MyTextField函数。你在相同位置渲染了一个MyComponent渲染时,都会创建一个不同的组件,因此React会重置其下的所有状态。这会导致错误和性能问题。为避免此问题,请始终在顶层声明组件函数,不要嵌套它们的定义。
在同一位置重置状态
默认情况下,当组件保持在相同位置时,React会保留其状态。通常,这正是你想要的,因此将其作为默认行为是合理的。但有时,你可能希望重置组件的状态。考虑这个应用程序,它允许两个玩家在每一轮中跟踪他们的得分:
目前,当你切换玩家时,分数会被保留。两个Counter组件出现在相同的位置,因此 React 将它们视为同一个Counter,只是其person属性发生了变化。
但从概念上讲,在这个应用中它们应该是两个独立的计数器。它们可能在用户界面的同一位置显示,但一个是 Taylor 的计数器,另一个是 Sarah 的计数器。
在它们之间切换时,有两种方法可以重置状态:
- 在不同位置渲染组件
- 使用
key为每个组件赋予明确的标识
选项 1:在不同位置渲染组件
如果你希望这两个Counter组件相互独立,你可以在两个不同的位置渲染它们:
- 最初,
isPlayerA为true。因此第一个位置包含Counter的状态,第二个位置为空。 - 当你点击“下一位玩家”按钮时,第一个位置被清空,但第二个位置现在包含一个
Counter。


初始状态


点击“下一位”


再次点击“下一位”
每个Counter的状态在其每次从 DOM 中移除时都会被销毁。这就是为什么每次点击按钮时它们都会重置。
当你只有少数几个独立的组件在同一位置渲染时,这种解决方案很方便。在这个例子中,你只有两个组件,所以在 JSX 中分别渲染它们并不麻烦。
方案二:使用 key 重置状态
还有另一种更通用的方法来重置组件的状态。
你可能在key渲染列表时见过键(key)。键不仅仅用于列表!你可以使用键来让 React 区分任何组件。默认情况下,React 使用组件在父组件中的顺序(“第一个计数器”、“第二个计数器”)来区分组件。但键让你可以告诉 React,这不仅仅是第一个计数器,或第二个计数器,而是一个特定的计数器——例如,泰勒的计数器。这样,无论泰勒的计数器出现在树中的哪个位置,React 都能识别它!
在这个例子中,两个<Counter />不共享状态,即使它们在 JSX 中的同一位置出现:
在泰勒和莎拉之间切换不会保留状态。这是因为你给它们指定了不同的key:
指定一个key会告诉 React 使用key本身作为位置的一部分,而不是它们在父组件中的顺序。这就是为什么,即使你在 JSX 中的同一位置渲染它们,React 也会将它们视为两个不同的计数器,因此它们永远不会共享状态。每次计数器出现在屏幕上时,其状态都会被创建。每次它被移除时,其状态都会被销毁。在它们之间切换会一遍又一遍地重置它们的状态。
注意
请记住,键不是全局唯一的。它们只指定在父组件内部的位置。
使用键重置表单
在处理表单时,使用键重置状态特别有用。
在这个聊天应用中,<Chat>组件包含文本输入状态:
尝试在输入框中输入一些内容,然后点击“Alice”或“Bob”选择不同的收件人。你会注意到输入框的状态被保留了,因为<Chat>组件在树中的同一位置被渲染。
在许多应用中,这可能是期望的行为,但在聊天应用中却不是!你不希望用户因为意外点击而将已输入的消息发送给错误的人。要修复这个问题,可以添加一个key:
这确保了当你选择不同的收件人时,Chat组件将从头开始重新创建,包括其下方树中的任何状态。React 也会重新创建 DOM 元素,而不是复用它们。
现在切换收件人总会清空文本框:
回顾
- 只要相同的组件在相同的位置渲染,React 就会保持其状态。
- 状态并不保存在 JSX 标签中。它与你在树中放置该 JSX 的位置相关联。
- 你可以通过给子树一个不同的 key 来强制其重置状态。
- 不要嵌套组件定义,否则你会意外重置状态。
Try out some challenges
Challenge 1 of 5:Fix disappearing input text #
This example shows a message when you press the button. However, pressing the button also accidentally resets the input. Why does this happen? Fix it so that pressing the button does not reset the input text.
