保留和重置状态

状态在组件之间是隔离的。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 何时选择保留或重置状态时

    ¥When React chooses to preserve or reset the state

  • 如何强制 React 重置组件的状态

    ¥How to force React to reset component’s state

  • 键和类型如何影响状态是否被保留

    ¥How keys and types affect whether the state is preserved

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

¥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,但了解它是如何工作的会很有用。

¥These are two separate counters because each is rendered at its own position in the tree. You don’t usually have to think about these positions to use React, but it can be useful to understand how it works.

在 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.
更新状态

只要你在树中的相同位置渲染相同的组件,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.
删除组件

当你勾选 “渲染第二个计数器” 时,第二个 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.
添加组件

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

¥React preserves a component’s state for as long as it’s being rendered at its position in the UI tree. If it gets removed, or a different component gets rendered at the same position, React discards its state.

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

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

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

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

易犯错误

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

¥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 组件都会返回 <div>,其中 <Counter /> 作为第一个子级。对于 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
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

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

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

单击复选框时,计数器状态会重置。尽管你渲染 Counter,但 div 的第一个子级从 div 变为 section。当子 div 从 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 div to a section. When the child div 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
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 在从树中移除组件时会破坏状态。

¥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.

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

¥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. 在不同位置渲染组件

    ¥Render components in different positions

  2. key 给每个组件一个明确的标识

    ¥Give each component an explicit identity with 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 状态,第二个是空的。

    ¥Initially, isPlayerA is true. So the first position contains Counter state, and the second one is empty.

  • 当你单击 “下一位玩家” 按钮时,第一个位置将被清除,但第二个位置现在包含 Counter

    ¥When you click the “Next player” button the first position clears but the second one now contains a 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 中删除时都会被销毁。这就是为什么每次单击按钮时它们都会重置。

¥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 的计数器。这样,React 就会知道 Taylor 计数器出现在树中的任何地方!

¥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

¥In many apps, this may be the desired behavior, but not in a chat app! You don’t want to let the user send a message they already typed to a wrong person due to an accidental click. To fix it, add a 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 节点,它会变得非常慢。

    ¥You could render all chats instead of just the current one, but hide all the others with CSS. The chats would not get removed from the tree, so their local state would be preserved. This solution works great for simple UIs. But it can get very slow if the hidden trees are large and contain a lot of DOM nodes.

  • 你可以 提升状态 并为父组件中的每个收件人保留待处理消息。这样,当子组件被删除时,这并不重要,因为保留重要信息的是父级。这是最常见的解决方案。

    ¥You could lift the state up and hold the pending message for each recipient in the parent component. This way, when the child components get removed, it doesn’t matter, because it’s the parent that keeps the important information. This is the most common solution.

  • 除了 React 状态之外,你还可以使用不同的来源。例如,你可能希望即使用户意外关闭页面也能保留消息草稿。要实现这一点,你可以让 Chat 组件通过读取 localStorage 来初始化其状态,并将草稿也保存在那里。

    ¥You might also use a different source in addition to React state. For example, you probably want a message draft to persist even if the user accidentally closes the page. To implement this, you could have the Chat component initialize its state by reading from the localStorage, and save the drafts there too.

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

¥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 就会保持状态。

    ¥React keeps state for as long as the same component is rendered at the same position.

  • 状态不保存在 JSX 标签中。它与你放置该 JSX 的树位置相关联。

    ¥State is not kept in JSX tags. It’s associated with the tree position in which you put that JSX.

  • 你可以通过给它一个不同的键来强制子树重置其状态。

    ¥You can force a subtree to reset its state by giving it a different key.

  • 不要嵌套组件定义,否则你会不小心重置状态。

    ¥Don’t nest component definitions, or you’ll reset state by accident.

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


React 中文网 - 粤ICP备13048890号