保留和重置状态

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

🌐 State is isolated between components. React keeps track of which state belongs to which component based on their place in the UI tree. You can control when to preserve state and when to reset it between re-renders.

你将学习到

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

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

🌐 State is tied to a position in the render tree

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

🌐 React builds render trees for the component structure in your UI.

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

🌐 When you give a component state, you might think the state “lives” inside the component. But the state is actually held inside React. React associates each piece of state it’s holding with the correct component by where that component sits in the render tree.

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

🌐 Here, there is only one <Counter /> JSX tag, but it’s rendered at two different positions:

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>
  );
}

这是树的样子:

🌐 Here’s how these look as a tree:

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 tree

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

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

🌐 In React, each component on the screen has fully isolated state. For example, if you render two Counter components side by side, each of them will get its own, independent, score and hover states.

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

🌐 Try clicking both counters and notice they don’t affect each other:

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>
  );
}

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

🌐 As you can see, when one counter is updated, only the state for that component is 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.
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.

更新状态

🌐 Updating state

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

🌐 React will keep the state around for as long as you render the same component at the same position in the tree. To see this, increment both counters, then remove the second component by unchecking “Render the second counter” checkbox, and then add it back by ticking it again:

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 移除一个组件时,它会销毁该组件的状态。

🌐 Notice how the moment you stop rendering the second counter, its state disappears completely. That’s because when React removes a component, it destroys its state.

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.

删除组件

🌐 Deleting a component

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

🌐 When you tick “Render the second counter”, a second Counter and its state are initialized from scratch (score = 0) and added to the 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.

添加组件

🌐 Adding a component

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

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

🌐 Same component at the same position preserves state

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

🌐 In this example, there are two different <Counter /> tags:

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 的第一个子元素:

🌐 When you tick or clear the checkbox, the counter state does not get reset. Whether isFancy is true or false, you always have a <Counter /> as the first child of the div returned from the root App component:

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 保持在相同的位置

🌐 Updating the App state does not reset the Counter because Counter stays in the same position

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

🌐 It’s the same component at the same position, so from React’s perspective, it’s the same counter.

易犯错误

记住,对 React 来说,重要的是在 UI 树中的位置——而不是在 JSX 标记中的位置! 这个组件有两个 return 条款,里面的 <Counter /> JSX 标签在 if 内部和外部各不相同:

🌐 Remember that it’s the position in the UI tree—not in the JSX markup—that matters to React! This component has two return clauses with different <Counter /> JSX tags inside and outside the if:

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 并不知道你在函数中放置条件的位置。它“看到”的只是你返回的树。

🌐 You might expect the state to reset when you tick checkbox, but it doesn’t! This is because both of these <Counter /> tags are rendered at the same position. React doesn’t know where you place the conditions in your function. All it “sees” is the tree you return.

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

🌐 In both cases, the App component returns a <div> with <Counter /> as a first child. To React, these two counters have the same “address”: the first child of the first child of the root. This is how React matches them up between the previous and next renders, regardless of how you structure your logic.

同一位置不同组件重置状态

🌐 Different components at the same position reset state

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

🌐 In this example, ticking the checkbox will replace <Counter> with a <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 并销毁了它的状态。

🌐 Here, you switch between different component types at the same position. Initially, the first child of the <div> contained a Counter. But when you swapped in a p, React removed the Counter from the UI tree and destroyed its state.

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 被添加

🌐 When Counter changes to p, the Counter is deleted and the p is added

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 被添加

🌐 When switching back, the p is deleted and the Counter is added

另外,**当你在同一位置渲染不同的组件时,它会重置其整个子树的状态。**要了解这是如何工作的,请先增加计数器,然后勾选复选框:

🌐 Also, when you render a different component in the same position, it resets the state of its entire subtree. To see how this works, increment the counter and then tick the checkbox:

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>
  );
}

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

🌐 The counter state gets reset when you click the checkbox. Although you render a Counter, the first child of the div changes from a section to a div. When the child section was removed from the DOM, the whole tree below it (including the Counter and its state) was destroyed as well.

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 会被添加

🌐 When section changes to div, the section is deleted and the new div is added

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 会被添加

🌐 When switching back, the div is deleted and the new section is added

通常情况下,如果你想在重新渲染之间保留状态,你的组件树结构需要在每次渲染之间“保持一致”。如果结构不同,状态会被销毁,因为当 React 从树中移除一个组件时会销毁它的状态。

🌐 As a rule of thumb, if you want to preserve the state between re-renders, the structure of your tree needs to “match up” from one render to another. If the structure is different, the state gets destroyed because React destroys state when it removes a component from the tree.

易犯错误

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

🌐 This is why you should not nest component function definitions.

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

🌐 Here, the MyTextField component function is defined inside 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 会重置下面的所有状态。这会导致错误和性能问题。为了避免这个问题,始终在顶层声明组件函数,并且不要嵌套定义它们。

🌐 Every time you click the button, the input state disappears! This is because a different MyTextField function is created for every render of MyComponent. You’re rendering a different component in the same position, so React resets all state below. This leads to bugs and performance problems. To avoid this problem, always declare component functions at the top level, and don’t nest their definitions.

在同一位置重置状态

🌐 Resetting state at the same position

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

🌐 By default, React preserves state of a component while it stays at the same position. Usually, this is exactly what you want, so it makes sense as the default behavior. But sometimes, you may want to reset a component’s state. Consider this app that lets two players keep track of their scores during each turn:

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 属性发生了变化。

🌐 Currently, when you change the player, the score is preserved. The two Counters appear in the same position, so React sees them as the same Counter whose person prop has changed.

但从概念上来说,在这个应用中它们应该是两个独立的计数器。它们可能在用户界面中出现在同一个位置,但一个是泰勒的计数器,另一个是萨拉的计数器。

🌐 But conceptually, in this app they should be two separate counters. They might appear in the same place in the UI, but one is a counter for Taylor, and another is a counter for Sarah.

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

🌐 There are two ways to reset state when switching between them:

  1. 在不同位置渲染组件
  2. 给每个组件一个明确的身份标识 key

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

🌐 Option 1: Rendering a component in different positions

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

🌐 If you want these two Counters to be independent, you can render them in two different positions:

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.

初始状态

🌐 Initial state

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.

点击“下一步”

🌐 Clicking “next”

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.

再次点击“下一步”

🌐 Clicking “next” again

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

🌐 Each Counter’s state gets destroyed each time it’s removed from the DOM. This is why they reset every time you click the button.

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

🌐 This solution is convenient when you only have a few independent components rendered in the same place. In this example, you only have two, so it’s not a hassle to render both separately in the JSX.

选项 2:使用键重置状态

🌐 Option 2: Resetting state with a key

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

🌐 There is also another, more generic, way to reset a component’s state.

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

🌐 You might have seen keys when rendering lists. Keys aren’t just for lists! You can use keys to make React distinguish between any components. By default, React uses order within the parent (“first counter”, “second counter”) to discern between components. But keys let you tell React that this is not just a first counter, or a second counter, but a specific counter—for example, Taylor’s counter. This way, React will know Taylor’s counter wherever it appears in the tree!

在这个例子中,这两个 <Counter /> 即使出现在 JSX 的同一个位置,也不会共享状态:

🌐 In this example, the two <Counter />s don’t share state even though they appear in the same place in 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

🌐 Switching between Taylor and Sarah does not preserve the state. This is because you gave them different keys:

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

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

🌐 Specifying a key tells React to use the key itself as part of the position, instead of their order within the parent. This is why, even though you render them in the same place in JSX, React sees them as two different counters, and so they will never share state. Every time a counter appears on the screen, its state is created. Every time it is removed, its state is destroyed. Toggling between them resets their state over and over.

注意

请记住,键在全局范围内并不唯一。它们仅指定在父元素中的位置

🌐 Remember that keys are not globally unique. They only specify the position within the parent.

使用键重置表单

🌐 Resetting a form with a key

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

🌐 Resetting state with a key is particularly useful when dealing with forms.

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

🌐 In this chat app, the <Chat> component contains the text input state:

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: 'taylor@mail.com' },
  { id: 1, name: 'Alice', email: 'alice@mail.com' },
  { id: 2, name: 'Bob', email: 'bob@mail.com' }
];

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

🌐 Try entering something into the input, and then press “Alice” or “Bob” to choose a different recipient. You will notice that the input state is preserved because the <Chat> is rendered at the same position in the tree.

在许多应用中,这可能是期望的行为,但在聊天应用中不是! 你不希望因为意外点击而让用户将已经输入的消息发送给错误的人。为了解决这个问题,添加一个 key

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

这确保了当你选择不同的接收者时,Chat 组件将从头重新创建,包括其下方的任何状态。React 还会重新创建 DOM 元素,而不是重复使用它们。

🌐 This ensures that when you select a different recipient, the Chat component will be recreated from scratch, including any state in the tree below it. React will also re-create the DOM elements instead of reusing them.

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

🌐 Now switching the recipient always clears the text field:

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: 'taylor@mail.com' },
  { id: 1, name: 'Alice', email: 'alice@mail.com' },
  { id: 2, name: 'Bob', email: 'bob@mail.com' }
];

深入研究

保留已删除组件的状态

🌐 Preserving state for removed components

在一个真实的聊天应用中,当用户再次选择之前的收件人时,你可能希望恢复输入状态。有几种方法可以让一个不再可见的组件保持“活跃”状态:

🌐 In a real chat app, you’d probably want to recover the input state when the user selects the previous recipient again. There are a few ways to keep the state “alive” for a component that’s no longer visible:

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

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

🌐 No matter which strategy you pick, a chat with Alice is conceptually distinct from a chat with Bob, so it makes sense to give a key to the <Chat> tree based on the current recipient.

回顾

  • 只要同一组件在同一位置渲染,React 就会保持状态。
  • 状态不会保存在 JSX 标签中。它与放置该 JSX 的树位置相关联。
  • 你可以通过给它一个不同的键来强制子树重置其状态。
  • 不要嵌套组件定义,否则你会不小心重置状态。

挑战 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.

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)}
    />
  );
}