保留和重置状态

组件之间的状态是隔离的。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,你总是在根App组件返回的div中拥有一个<Counter />作为第一个子元素

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

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

在这个例子中,勾选复选框会用<p>替换<Counter>

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中渲染它们并不麻烦。

方案二:使用key重置状态

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

您可能在渲染列表时看到过key。key不仅仅用于列表!您可以使用key让React区分任何组件。默认情况下,React使用父元素中的顺序(“第一个计数器”、“第二个计数器”)来区分组件。但是,key可以让您告诉React,这不是仅仅是第一个计数器或第二个计数器,而是一个特定的计数器——例如,Taylor的计数器。这样,React就能在树中的任何位置识别Taylor的计数器!

在这个例子中,两个<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 的树状结构位置相关联。
  • 可以通过赋予子树不同的键值来强制其重置状态。
  • 不要嵌套组件定义,否则会意外地重置状态。

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