v19.2Latest

保留与重置状态

状态在组件之间是隔离的。React 会根据组件在 UI 树中的位置来跟踪哪个状态属于哪个组件。你可以控制在重新渲染时何时保留状态以及何时重置它。

你将学习
  • React 何时选择保留或重置状态
  • 如何强制 React 重置组件的状态
  • 键(key)和类型如何影响状态是否被保留

状态与渲染树中的位置相关联

React 会为你的 UI 中的组件结构构建渲染树

当你给组件添加状态时,你可能会认为状态“存在于”组件内部。但实际上状态是由 React 持有的。React 通过组件在渲染树中的位置,将其持有的每一块状态与正确的组件关联起来。

这里只有一个<Counter />JSX 标签,但它在两个不同的位置被渲染:

它们在树中的结构如下:

React 组件树的示意图。根节点标记为 'div',有两个子节点。每个子节点都标记为 'Counter',并且都包含一个标记为 'count'、值为 0 的状态气泡。React 组件树的示意图。根节点标记为 'div',有两个子节点。每个子节点都标记为 'Counter',并且都包含一个标记为 'count'、值为 0 的状态气泡。

React 树

这是两个独立的计数器,因为每个都在树中的各自位置上被渲染。在使用 React 时,你通常不需要考虑这些位置,但了解其工作原理可能很有用。

在 React 中,屏幕上的每个组件都有完全隔离的状态。例如,如果你并排渲染两个Counter组件,它们各自都会获得自己独立的scorehover状态。

尝试点击两个计数器,注意它们互不影响:

如你所见,当一个计数器被更新时,只有该组件的状态会被更新:

React 组件树的示意图。根节点标记为 'div',有两个子节点。左子节点标记为 'Counter',包含一个标记为 'count' 的状态气泡,值为 0。右子节点标记为 'Counter',包含一个标记为 'count' 的状态气泡,值为 1。右子节点的状态气泡以黄色高亮显示,表示其值已更新。React 组件树的示意图。根节点标记为 'div',有两个子节点。左子节点标记为 'Counter',包含一个标记为 'count' 的状态气泡,值为 0。右子节点标记为 'Counter',包含一个标记为 'count' 的状态气泡,值为 1。右子节点的状态气泡以黄色高亮显示,表示其值已更新。

更新状态

只要你在树中的同一位置渲染相同的组件,React 就会保持其状态。要验证这一点,请先递增两个计数器,然后通过取消勾选“渲染第二个计数器”复选框来移除第二个组件,接着再勾选它以重新添加回来:

请注意,当你停止渲染第二个计数器的那一刻,它的状态就完全消失了。这是因为当 React 移除一个组件时,它会销毁其状态。

React 组件树的示意图。根节点标记为 'div',有两个子节点。左子节点标记为 'Counter',包含一个标记为 'count' 的状态气泡,值为 0。右子节点缺失,其位置显示一个黄色的 '噗' 消失图像,高亮显示该组件正从树中被删除。React 组件树的示意图。根节点标记为 'div',有两个子节点。左子节点标记为 'Counter',包含一个标记为 'count' 的状态气泡,值为 0。右子节点缺失,其位置有一个黄色的 '噗' 图像,高亮显示该组件正从树中被删除。

删除组件

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

React 组件树的示意图。根节点标记为 'div',有两个子节点。左子节点标记为 'Counter',包含一个标记为 'count' 的状态气泡,值为 0。右子节点标记为 'Counter',包含一个标记为 'count' 的状态气泡,值为 0。整个右子节点高亮为黄色,表示它刚刚被添加到树中。React 组件树的示意图。根节点标记为 'div',有两个子节点。左子节点标记为 'Counter',包含一个标记为 'count' 的状态气泡,值为 0。右子节点标记为 'Counter',包含一个标记为 'count' 的状态气泡,值为 0。整个右子节点高亮为黄色,表示它刚刚被添加到树中。

添加组件

只要组件在 UI 树中的位置被渲染,React 就会保留其状态。如果它被移除,或者在相同位置渲染了不同的组件,React 会丢弃其状态。

相同位置下的相同组件会保留状态

在这个例子中,有两个不同的<Counter />标签:

当你勾选或清除复选框时,计数器状态不会重置。无论 isFancytrue还是false,你始终有一个<Counter /> 作为根 div 返回的 App组件的第一个子组件:

由箭头分隔并过渡的两个部分的图表。每个部分包含一个组件布局,其中有一个父组件标记为'App',包含一个标记为isFancy的状态气泡。该组件有一个标记为'div'的子组件,它指向一个包含isFancy(以紫色高亮显示)的属性气泡,该属性被传递给唯一的子组件。最后一个子组件标记为'Counter',包含一个标记为'count'的状态气泡,两个图表中的值都是3。在图表的左侧部分,没有内容被高亮显示,父状态isFancy的值为false。在图表的右侧部分,父状态isFancy的值已变为true并以黄色高亮显示,其下方的属性气泡也同样高亮显示,其isFancy值也已变为true。由箭头分隔并过渡的两个部分的图表。每个部分包含一个组件布局,其中有一个父组件标记为'App',包含一个标记为isFancy的状态气泡。该组件有一个标记为'div'的子组件,它指向一个包含isFancy(以紫色高亮显示)的属性气泡,该属性被传递给唯一的子组件。最后一个子组件标记为'Counter',包含一个标记为'count'的状态气泡,两个图表中的值都是3。在图表的左侧部分,没有内容被高亮显示,父状态isFancy的值为false。在图表的右侧部分,父状态isFancy的值已变为true并以黄色高亮显示,其下方的属性气泡也同样高亮显示,其isFancy值也已变为true。

更新 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并销毁了它的状态。

包含三个部分的图表,各部分之间有箭头过渡。第一部分包含一个标记为'div'的 React 组件,其单个子组件标记为'Counter',内含一个标记为'count'、值为 3 的状态气泡。中间部分具有相同的'div'父组件,但子组件现已被删除,由黄色'proof'图像表示。第三部分再次具有相同的'div'父组件,现在有一个标记为'p'的新子组件,以黄色高亮显示。包含三个部分的图表,各部分之间有箭头过渡。第一部分包含一个标记为'div'的 React 组件,其单个子组件标记为'Counter',内含一个标记为'count'、值为 3 的状态气泡。中间部分具有相同的'div'父组件,但子组件现已被删除,由黄色'proof'图像表示。第三部分再次具有相同的'div'父组件,现在有一个标记为'p'的新子组件,以黄色高亮显示。

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

包含三个部分的图表,各部分之间有箭头过渡。第一部分包含一个标记为'p'的 React 组件。中间部分具有相同的'div'父组件,但子组件现已被删除,由黄色'proof'图像表示。第三部分再次具有相同的'div'父组件,现在有一个标记为'Counter'的新子组件,内含一个标记为'count'、值为 0 的状态气泡,以黄色高亮显示。包含三个部分的图表,各部分之间有箭头过渡。第一部分包含一个标记为'p'的 React 组件。中间部分具有相同的'div'父组件,但子组件现已被删除,由黄色'proof'图像表示。第三部分再次具有相同的'div'父组件,现在有一个标记为'Counter'的新子组件,内含一个标记为'count'、值为 0 的状态气泡,以黄色高亮显示。

当切换回来时,p被删除,Counter被添加

此外,当你在同一位置渲染不同的组件时,它会重置其整个子树的状态。要了解其工作原理,请递增计数器然后勾选复选框:

当你点击复选框时,计数器状态会被重置。虽然你渲染了一个Counter,但div的第一个子元素从section变为div。当子元素section从 DOM 中移除时,其下方的整个树(包括Counter及其状态)也随之被销毁。

包含三个部分的图表,各部分之间有箭头过渡。第一部分包含一个标记为'div'的 React 组件,其单个子组件标记为'section',该子组件又有一个标记为'Counter'的单个子组件,内含一个标记为'count'、值为 3 的状态气泡。中间部分具有相同的'div'父组件,但子组件现已被删除,由黄色'proof'图像表示。第三部分再次具有相同的'div'父组件,现在有一个标记为'div'的新子组件(以黄色高亮显示),该子组件也有一个标记为'Counter'的新子组件,内含一个标记为'count'、值为 0 的状态气泡,全部以黄色高亮显示。包含三个部分的图表,各部分之间有箭头过渡。第一部分包含一个标记为'div'的 React 组件,其单个子组件标记为'section',该子组件又有一个标记为'Counter'的单个子组件,内含一个标记为'count'、值为 3 的状态气泡。中间部分具有相同的'div'父组件,但子组件现已被删除,由黄色'proof'图像表示。第三部分再次具有相同的'div'父组件,现在有一个标记为'div'的新子组件(以黄色高亮显示),该子组件也有一个标记为'Counter'的新子组件,内含一个标记为'count'、值为 0 的状态气泡,全部以黄色高亮显示。

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

包含三个部分的图表,各部分之间有箭头过渡。第一部分包含一个标记为'div'的React组件,它有一个标记为'div'的子组件,该子组件又有一个标记为'Counter'的子组件,其中包含一个标记为'count'的状态气泡,值为0。中间部分具有相同的'div'父组件,但子组件现已被删除,用黄色的'proof'图像表示。第三部分再次具有相同的'div'父组件,现在有一个标记为'section'的新子组件(高亮为黄色),还有一个标记为'Counter'的新子组件,其中包含一个标记为'count'的状态气泡,值为0,全部高亮为黄色。包含三个部分的图表,各部分之间有箭头过渡。第一部分包含一个标记为'div'的React组件,它有一个标记为'div'的子组件,该子组件又有一个标记为'Counter'的子组件,其中包含一个标记为'count'的状态气泡,值为0。中间部分具有相同的'div'父组件,但子组件现已被删除,用黄色的'proof'图像表示。第三部分再次具有相同的'div'父组件,现在有一个标记为'section'的新子组件(高亮为黄色),还有一个标记为'Counter'的新子组件,其中包含一个标记为'count'的状态气泡,值为0,全部高亮为黄色。

当切换回来时,div被删除,新的section被添加

根据经验法则,如果你想在重新渲染之间保留状态,你的树结构需要在两次渲染之间“匹配”。如果结构不同,状态就会被销毁,因为当React从树中移除一个组件时,它会销毁该组件的状态。

陷阱

这就是为什么你不应该嵌套组件函数定义。

在这里,MyTextField组件函数定义在MyComponent内部:

每次点击按钮时,输入状态都会消失!这是因为每次不同的MyTextField函数。你在相同位置渲染了一个MyComponent渲染时,都会创建一个不同的组件,因此React会重置其下的所有状态。这会导致错误和性能问题。为避免此问题,请始终在顶层声明组件函数,不要嵌套它们的定义。

在同一位置重置状态

默认情况下,当组件保持在相同位置时,React会保留其状态。通常,这正是你想要的,因此将其作为默认行为是合理的。但有时,你可能希望重置组件的状态。考虑这个应用程序,它允许两个玩家在每一轮中跟踪他们的得分:

目前,当你切换玩家时,分数会被保留。两个Counter组件出现在相同的位置,因此 React 将它们视为同一个Counter,只是其person属性发生了变化。

但从概念上讲,在这个应用中它们应该是两个独立的计数器。它们可能在用户界面的同一位置显示,但一个是 Taylor 的计数器,另一个是 Sarah 的计数器。

在它们之间切换时,有两种方法可以重置状态:

  1. 在不同位置渲染组件
  2. 使用key为每个组件赋予明确的标识

选项 1:在不同位置渲染组件

如果你希望这两个Counter组件相互独立,你可以在两个不同的位置渲染它们:

  • 最初,isPlayerAtrue。因此第一个位置包含Counter的状态,第二个位置为空。
  • 当你点击“下一位玩家”按钮时,第一个位置被清空,但第二个位置现在包含一个Counter
展示 React 组件树的图表。父组件标记为 'Scoreboard',带有一个状态气泡,标记为 isPlayerA,值为 'true'。唯一的子组件(排列在左侧)标记为 Counter,带有一个状态气泡,标记为 'count',值为 0。左侧子组件的所有部分均以黄色高亮显示,表示它被添加了。展示 React 组件树的图表。父组件标记为 'Scoreboard',带有一个状态气泡,标记为 isPlayerA,值为 'true'。唯一的子组件(排列在左侧)标记为 Counter,带有一个状态气泡,标记为 'count',值为 0。左侧子组件的所有部分均以黄色高亮显示,表示它被添加了。

初始状态

展示 React 组件树的图表。父组件标记为 'Scoreboard',带有一个状态气泡,标记为 isPlayerA,值为 'false'。状态气泡以黄色高亮显示,表示它已更改。左侧子组件被替换为一个黄色的 'poof' 图像,表示它已被删除,并且在右侧有一个新的子组件,以黄色高亮显示,表示它被添加了。新的子组件标记为 'Counter',并包含一个状态气泡,标记为 'count',值为 0。展示 React 组件树的图表。父组件标记为 'Scoreboard',带有一个状态气泡,标记为 isPlayerA,值为 'false'。状态气泡以黄色高亮显示,表示它已更改。左侧子组件被替换为一个黄色的 'poof' 图像,表示它已被删除,并且在右侧有一个新的子组件,以黄色高亮显示,表示它被添加了。新的子组件标记为 'Counter',并包含一个状态气泡,标记为 'count',值为 0。

点击“下一位”

展示 React 组件树的图表。父组件标记为 'Scoreboard',带有一个状态气泡,标记为 isPlayerA,值为 'true'。状态气泡以黄色高亮显示,表示它已更改。左侧有一个新的子组件,以黄色高亮显示,表示它被添加了。新的子组件标记为 'Counter',并包含一个状态气泡,标记为 'count',值为 0。右侧子组件被替换为一个黄色的 'poof' 图像,表示它已被删除。展示 React 组件树的图表。父组件标记为 'Scoreboard',带有一个状态气泡,标记为 isPlayerA,值为 'true'。状态气泡以黄色高亮显示,表示它已更改。左侧有一个新的子组件,以黄色高亮显示,表示它被添加了。新的子组件标记为 'Counter',并包含一个状态气泡,标记为 'count',值为 0。右侧子组件被替换为一个黄色的 'poof' 图像,表示它已被删除。

再次点击“下一位”

每个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 元素,而不是复用它们。

现在切换收件人总会清空文本框:

Deep Dive
为已移除的组件保留状态

回顾

  • 只要相同的组件在相同的位置渲染,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.