保留和重置状态

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

您将学习

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

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

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

当您为组件提供状态时,您可能会认为状态“存在于”组件内部。但状态实际上是保存在 React 内部。React 通过组件在渲染树中的位置将它持有的每个状态片段与正确的组件相关联。

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

import { useState } from 'react';

export default function App() {
  const counter = <Counter />;
  return (
    <div>
      {counter}
      {counter}
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

以下是它们作为树的外观

Diagram of a tree of React components. The root node is labeled 'div' and has two children. Each of the children are labeled 'Counter' and both contain a state bubble labeled 'count' with value 0.
Diagram of a tree of React components. The root node is labeled 'div' and has two children. Each of the children are labeled 'Counter' and both contain a state bubble labeled 'count' with value 0.

React 树

这是两个独立的计数器,因为每个计数器都在树中的自身位置渲染。 您通常不必考虑这些位置就可以使用 React,但了解它的工作原理可能很有用。

在 React 中,屏幕上的每个组件都有完全隔离的状态。例如,如果您并排渲染两个 Counter 组件,则每个组件都将获得其自身独立的 scorehover 状态。

尝试单击两个计数器,并注意它们不会互相影响

import { useState } from 'react';

export default function App() {
  return (
    <div>
      <Counter />
      <Counter />
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

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

Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is labeled 'Counter' and contains a state bubble labeled 'count' with value 1. The state bubble of the right child is highlighted in yellow to indicate its value has updated.
Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is labeled 'Counter' and contains a state bubble labeled 'count' with value 1. The state bubble of the right child is highlighted in yellow to indicate its value has updated.

更新状态

只要您在树中的相同位置渲染相同的组件,React 就会一直保留该状态。要查看这一点,请增加两个计数器的值,然后通过取消选中“渲染第二个计数器”复选框删除第二个组件,然后通过再次选中它来添加它

import { useState } from 'react';

export default function App() {
  const [showB, setShowB] = useState(true);
  return (
    <div>
      <Counter />
      {showB && <Counter />} 
      <label>
        <input
          type="checkbox"
          checked={showB}
          onChange={e => {
            setShowB(e.target.checked)
          }}
        />
        Render the second counter
      </label>
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

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

Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is missing, and in its place is a yellow 'poof' image, highlighting the component being deleted from the tree.
Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is missing, and in its place is a yellow 'poof' image, highlighting the component being deleted from the tree.

删除组件

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

Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The entire right child node is highlighted in yellow, indicating that it was just added to the tree.
Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The entire right child node is highlighted in yellow, indicating that it was just added to the tree.

添加组件

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

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

在此示例中,有两个不同的 <Counter /> 标签

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <Counter isFancy={true} /> 
      ) : (
        <Counter isFancy={false} /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Use fancy styling
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

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

Diagram with two sections separated by an arrow transitioning between them. Each section contains a layout of components with a parent labeled 'App' containing a state bubble labeled isFancy. This component has one child labeled 'div', which leads to a prop bubble containing isFancy (highlighted in purple) passed down to the only child. The last child is labeled 'Counter' and contains a state bubble with label 'count' and value 3 in both diagrams. In the left section of the diagram, nothing is highlighted and the isFancy parent state value is false. In the right section of the diagram, the isFancy parent state value has changed to true and it is highlighted in yellow, and so is the props bubble below, which has also changed its isFancy value to true.
Diagram with two sections separated by an arrow transitioning between them. Each section contains a layout of components with a parent labeled 'App' containing a state bubble labeled isFancy. This component has one child labeled 'div', which leads to a prop bubble containing isFancy (highlighted in purple) passed down to the only child. The last child is labeled 'Counter' and contains a state bubble with label 'count' and value 3 in both diagrams. In the left section of the diagram, nothing is highlighted and the isFancy parent state value is false. In the right section of the diagram, the isFancy parent state value has changed to true and it is highlighted in yellow, and so is the props bubble below, which has also changed its isFancy value to true.

更新 App 状态不会重置 Counter,因为 Counter 保持在相同的位置

它是相同位置的相同组件,因此从 React 的角度来看,它是同一个计数器。

陷阱

请记住,对 React 来说,重要的是在 UI 树中的位置,而不是在 JSX 标记中的位置! 此组件有两个 return 子句,在 if 的内部和外部具有不同的 <Counter /> JSX 标签。

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  if (isFancy) {
    return (
      <div>
        <Counter isFancy={true} />
        <label>
          <input
            type="checkbox"
            checked={isFancy}
            onChange={e => {
              setIsFancy(e.target.checked)
            }}
          />
          Use fancy styling
        </label>
      </div>
    );
  }
  return (
    <div>
      <Counter isFancy={false} />
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Use fancy styling
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

你可能期望在勾选复选框时状态会重置,但它不会!这是因为 这两个 <Counter /> 标签都渲染在相同的位置。 React 并不知道你在函数中放置条件的位置。它“看到”的只是你返回的树。

在这两种情况下,App 组件都返回一个 <div>,其中 <Counter /> 作为第一个子元素。对于 React 来说,这两个计数器具有相同的“地址”:根元素的第一个子元素的第一个子元素。这就是 React 在上一次和下一次渲染之间匹配它们的方式,而不管你如何构建逻辑。

相同位置的不同组件会重置状态

在此示例中,勾选复选框会将 <Counter> 替换为 <p>

import { useState } from 'react';

export default function App() {
  const [isPaused, setIsPaused] = useState(false);
  return (
    <div>
      {isPaused ? (
        <p>See you later!</p> 
      ) : (
        <Counter /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isPaused}
          onChange={e => {
            setIsPaused(e.target.checked)
          }}
        />
        Take a break
      </label>
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

在这里,你在相同位置切换了*不同*的组件类型。最初,<div> 的第一个子元素包含一个 Counter。但是当你将其替换为 p 时,React 会从 UI 树中移除 Counter 并销毁其状态。

Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'Counter' containing a state bubble labeled 'count' with value 3. The middle section has the same 'div' parent, but the child component has now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'p', highlighted in yellow.
Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'Counter' containing a state bubble labeled 'count' with value 3. The middle section has the same 'div' parent, but the child component has now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'p', highlighted in yellow.

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

Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'p'. The middle section has the same 'div' parent, but the child component has now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, highlighted in yellow.
Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'p'. The middle section has the same 'div' parent, but the child component has now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, highlighted in yellow.

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

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

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <div>
          <Counter isFancy={true} /> 
        </div>
      ) : (
        <section>
          <Counter isFancy={false} />
        </section>
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Use fancy styling
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

当你单击复选框时,计数器状态将重置。尽管你渲染了一个 Counter,但 <div> 的第一个子元素从 div 变为 section。当子 div 从 DOM 中移除时,它下面的整个树(包括 Counter 及其状态)也被销毁了。

Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'section', which has a single child labeled 'Counter' containing a state bubble labeled 'count' with value 3. The middle section has the same 'div' parent, but the child components have now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'div', highlighted in yellow, also with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, all highlighted in yellow.
Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'section', which has a single child labeled 'Counter' containing a state bubble labeled 'count' with value 3. The middle section has the same 'div' parent, but the child components have now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'div', highlighted in yellow, also with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, all highlighted in yellow.

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

Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'div', which has a single child labeled 'Counter' containing a state bubble labeled 'count' with value 0. The middle section has the same 'div' parent, but the child components have now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'section', highlighted in yellow, also with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, all highlighted in yellow.
Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'div', which has a single child labeled 'Counter' containing a state bubble labeled 'count' with value 0. The middle section has the same 'div' parent, but the child components have now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'section', highlighted in yellow, also with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, all highlighted in yellow.

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

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

陷阱

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

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

import { useState } from 'react';

export default function MyComponent() {
  const [counter, setCounter] = useState(0);

  function MyTextField() {
    const [text, setText] = useState('');

    return (
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
    );
  }

  return (
    <>
      <MyTextField />
      <button onClick={() => {
        setCounter(counter + 1)
      }}>Clicked {counter} times</button>
    </>
  );
}

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

在相同位置重置状态

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

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA ? (
        <Counter person="Taylor" />
      ) : (
        <Counter person="Sarah" />
      )}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{person}'s score: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

目前,当你更换玩家时,分数会保留。这两个 Counter 出现在相同的位置,因此 React 将它们视为*相同*的 Counter,其 person 属性已更改。

但从概念上讲,在这个应用程序中,它们应该是两个独立的计数器。它们可能出现在 UI 中的同一个位置,但一个是 Taylor 的计数器,另一个是 Sarah 的计数器。

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

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

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

如果你希望这两个 Counter 是独立的,你可以将它们渲染在两个不同的位置。

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA &&
        <Counter person="Taylor" />
      }
      {!isPlayerA &&
        <Counter person="Sarah" />
      }
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{person}'s score: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

  • 最初,isPlayerAtrue。所以第一个位置包含 Counter 状态,第二个位置为空。
  • 当你单击“下一个玩家”按钮时,第一个位置将清除,但第二个位置现在包含一个 Counter
Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'true'. The only child, arranged to the left, is labeled Counter with a state bubble labeled 'count' and value 0. All of the left child is highlighted in yellow, indicating it was added.
Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'true'. The only child, arranged to the left, is labeled Counter with a state bubble labeled 'count' and value 0. All of the left child is highlighted in yellow, indicating it was added.

初始状态

Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'false'. The state bubble is highlighted in yellow, indicating that it has changed. The left child is replaced with a yellow 'poof' image indicating that it has been deleted and there is a new child on the right, highlighted in yellow indicating that it was added. The new child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0.
Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'false'. The state bubble is highlighted in yellow, indicating that it has changed. The left child is replaced with a yellow 'poof' image indicating that it has been deleted and there is a new child on the right, highlighted in yellow indicating that it was added. The new child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0.

单击“下一个”

Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'true'. The state bubble is highlighted in yellow, indicating that it has changed. There is a new child on the left, highlighted in yellow indicating that it was added. The new child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is replaced with a yellow 'poof' image indicating that it has been deleted.
Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'true'. The state bubble is highlighted in yellow, indicating that it has changed. There is a new child on the left, highlighted in yellow indicating that it was added. The new child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is replaced with a yellow 'poof' image indicating that it has been deleted.

再次单击“下一个”

每个 Counter 的状态在每次从 DOM 中移除时都会被销毁。这就是为什么每次单击按钮时它们都会重置。

当你只有几个独立的组件渲染在同一个位置时,此解决方案很方便。在此示例中,你只有两个,因此在 JSX 中分别渲染它们并不麻烦。

选项 2:使用 Key 重置状态

还有一种更通用的方法来重置组件的状态。

您可能在渲染列表时见过key。Key 不仅仅用于列表!您可以使用 key 来使 React 区分任何组件。默认情况下,React 使用父组件中的顺序(“第一个计数器”、“第二个计数器”)来区分组件。但是,key 允许您告诉 React,这不仅仅是*第一个*计数器或*第二个*计数器,而是一个特定的计数器——例如,*Taylor 的*计数器。这样,无论*Taylor 的*计数器出现在树中的哪个位置,React 都能识别它!

在此示例中,即使两个 <Counter /> 出现在 JSX 中的相同位置,它们也不会共享状态

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA ? (
        <Counter key="Taylor" person="Taylor" />
      ) : (
        <Counter key="Sarah" person="Sarah" />
      )}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{person}'s score: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

在 Taylor 和 Sarah 之间切换不会保留状态。这是因为您为它们提供了不同的 key

{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}

指定 key 会告诉 React 使用 key 本身作为位置的一部分,而不是它们在父组件中的顺序。这就是为什么,即使您在 JSX 中的相同位置渲染它们,React 也会将它们视为两个不同的计数器,因此它们永远不会共享状态。每次计数器出现在屏幕上时,都会创建其状态。每次将其删除时,其状态都会被销毁。在它们之间切换会反复重置它们的状态。

注意

请记住,key 不是全局唯一的。它们仅指定*在父组件内*的位置。

使用 Key 重置表单

在处理表单时,使用 key 重置状态特别有用。

在这个聊天应用程序中,<Chat> 组件包含文本输入状态

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat contact={to} />
    </div>
  )
}

const contacts = [
  { id: 0, name: 'Taylor', email: '[email protected]' },
  { id: 1, name: 'Alice', email: '[email protected]' },
  { id: 2, name: 'Bob', email: '[email protected]' }
];

尝试在输入框中输入一些内容,然后按“Alice”或“Bob”选择不同的收件人。您会注意到输入状态被保留,因为 <Chat> 渲染在树中的相同位置。

在许多应用程序中,这可能是预期的行为,但在聊天应用程序中则不然! 您不希望因为用户意外点击而让他们将已经输入的信息发送给错误的人。要解决此问题,请添加一个 key

<Chat key={to.id} contact={to} />

这可确保在您选择其他收件人时,将从头开始重新创建 Chat 组件,包括其下方树中的任何状态。React 还会重新创建 DOM 元素,而不是重复使用它们。

现在切换收件人总是会清除文本字段

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat key={to.id} contact={to} />
    </div>
  )
}

const contacts = [
  { id: 0, name: 'Taylor', email: '[email protected]' },
  { id: 1, name: 'Alice', email: '[email protected]' },
  { id: 2, name: 'Bob', email: '[email protected]' }
];

深入探讨

为已删除的组件保留状态

在真实的聊天应用程序中,当用户再次选择上一个收件人时,您可能希望恢复输入状态。有一些方法可以为不再可见的组件保留状态的“活动”

  • 您可以渲染*所有*聊天,而不仅仅是当前聊天,但使用 CSS 隐藏所有其他聊天。聊天不会从树中删除,因此它们的本地状态将被保留。此解决方案非常适用于简单的 UI。但是,如果隐藏的树很大并且包含许多 DOM 节点,它可能会变得非常慢。
  • 您可以将状态提升并在父组件中为每个收件人保留待处理消息。这样,当子组件被删除时,它并不重要,因为保留重要信息的是父组件。这是最常见的解决方案。
  • 除了 React 状态之外,您还可以使用其他来源。例如,您可能希望即使用户意外关闭页面也能保留消息草稿。要实现这一点,您可以让 Chat 组件通过从 localStorage 读取来初始化其状态,并在那里保存草稿。

无论您选择哪种策略,*与 Alice 的*聊天在概念上都不同于*与 Bob 的*聊天,因此根据当前收件人为 <Chat> 树提供 key 是有意义的。

总结

  • 只要在相同位置渲染相同的组件,React 就会保留其状态。
  • 状态不保存在 JSX 标签中。它与您放置该 JSX 的树位置相关联。
  • 您可以通过为子树提供不同的 key 来强制其重置状态。
  • 不要嵌套组件定义,否则您会意外重置状态。

挑战 1 5:
修复消失的输入文本

此示例在您按下按钮时显示一条消息。但是,按下按钮也会意外重置输入。为什么会发生这种情况?修复它,以便按下按钮不会重置输入文本。

import { useState } from 'react';

export default function App() {
  const [showHint, setShowHint] = useState(false);
  if (showHint) {
    return (
      <div>
        <p><i>Hint: Your favorite city?</i></p>
        <Form />
        <button onClick={() => {
          setShowHint(false);
        }}>Hide hint</button>
      </div>
    );
  }
  return (
    <div>
      <Form />
      <button onClick={() => {
        setShowHint(true);
      }}>Show hint</button>
    </div>
  );
}

function Form() {
  const [text, setText] = useState('');
  return (
    <textarea
      value={text}
      onChange={e => setText(e.target.value)}
    />
  );
}