保存和重置狀態

元件之間的狀態是隔離的。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 子句,其中包含不同的 <Counter /> JSX 標籤,位於 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 不知道您在函式中放置條件的位置。它「看到」的只是您返回的樹狀結構。

在這兩種情況下,`App` 元件都會回傳一個 `<div>`,其中 `<Counter />` 作為第一個子元件。對 React 來說,這兩個計數器具有相同的「地址」:根元件的第一個子元件的第一個子元件。這就是 React 在先前和下一次渲染之間匹配它們的方式,無論您如何組織邏輯。

相同位置的不同元件會重置狀態

在此範例中,勾選核取方塊會將 `<Counter>` 替換為 `<p>`

import { useState } from 'react';

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

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

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

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

在這裡,您在相同位置切換*不同*的元件類型。最初,`<div>` 的第一個子元件包含一個 `Counter`。但是當您換成 `p` 時,React 會從 UI 樹狀結構中移除 `Counter` 並銷毀其狀態。

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

當 `Counter` 更改為 `p` 時,`Counter` 會被刪除,而 `p` 會被新增

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

切換回來時,`p` 會被刪除,而 `Counter` 會被新增

此外,**當您在相同位置渲染不同的元件時,它會重置其整個子樹狀結構的狀態。** 若要查看其運作方式,請增加計數器,然後勾選核取方塊

import { useState } from 'react';

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

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

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

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

當您點擊核取方塊時,計數器狀態會被重置。雖然您渲染了一個 `Counter`,但 `div` 的第一個子元件會從 `div` 更改為 `section`。當子 `div` 從 DOM 中移除時,它下面的整個樹狀結構(包括 `Counter` 及其狀態)也會被銷毀。

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

當 `section` 更改為 `div` 時,`section` 會被刪除,而新的 `div` 會被新增

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

切換回來時,`div` 會被刪除,而新的 `section` 會被新增

根據經驗法則,**如果您想在重新渲染之間保留狀態,您的樹狀結構必須在**一次渲染到下一次渲染之間「匹配」。如果結構不同,狀態就會被銷毀,因為 React 在從樹狀結構中移除元件時會銷毀狀態。

陷阱

這就是為什麼您不應該巢式化元件函式定義的原因。

在這裡,`MyTextField` 元件函式定義在 `MyComponent` *內部*

import { useState } from 'react';

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

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

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

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

每次您點擊按鈕時,輸入狀態都會消失!這是因為每次渲染 `MyComponent` 時都會建立一個*不同*的 `MyTextField` 函式。您在相同位置渲染*不同*的元件,因此 React 會重置下面的所有狀態。這會導致錯誤和效能問題。為了避免這個問題,**始終在頂層宣告元件函式,並且不要巢式化它們的定義。**

在相同位置重置狀態

預設情況下,React 會在元件保持在相同位置時保留其狀態。通常,這正是您想要的,因此將其設為預設行為是有道理的。但是有時候,您可能想要重置元件的狀態。請考慮這個應用程式,它允許兩位玩家在每個回合中追蹤他們的分數

import { useState } from 'react';

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

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

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

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

目前,當您更改玩家時,分數會被保留。這兩個 `Counter` 出現在相同位置,因此 React 將它們視為*相同*的 `Counter`,其 `person` 屬性已更改。

但在概念上,在此應用程式中,它們應該是兩個獨立的計數器。它們可能出現在 UI 中的同一個位置,但一個是 Taylor 的計數器,另一個是 Sarah 的計數器。

在它們之間切換時,有兩種方法可以重置狀態

  1. 在不同位置渲染元件
  2. 使用 `key` 為每個元件指定明確的識別

選項 1:在不同位置渲染元件

如果您希望這兩個 `Counter` 獨立,您可以將它們渲染在兩個不同的位置

import { useState } from 'react';

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

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

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

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

  • 最初,`isPlayerA` 為 `true`。因此,第一個位置包含 `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.

再次點擊「下一位」

每次從 DOM 中移除時,每個 `Counter` 的狀態都會被銷毀。這就是為什麼每次您點擊按鈕時它們都會重置的原因。

當您只在同一個位置渲染少數幾個獨立元件時,此解決方案很方便。在此範例中,您只有兩個,因此在 JSX 中分別渲染它們並不麻煩。

選項 2:使用 key 重置狀態

還有另一種更通用的方法可以重置元件的狀態。

您可能在渲染列表時看過 key。Keys 不僅僅適用於列表!您可以使用 keys 來讓 React 區分任何元件。預設情況下,React 使用父元件中的順序(「第一個計數器」、「第二個計數器」)來區分元件。但 keys 讓您可以告訴 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 也會將它們視為兩個不同的計數器,因此它們永遠不會共享狀態。每次計數器出現在螢幕上時,都會建立其狀態。每次移除它時,它的狀態都會被銷毀。在它們之間切換會不斷重置它們的狀態。

注意事項

請記住,keys 不是全域唯一的。它們僅指定父元件內的位置。

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

嘗試在輸入框中輸入一些內容,然後按下「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: 'taylor@mail.com' },
  { id: 1, name: 'Alice', email: 'alice@mail.com' },
  { id: 2, name: 'Bob', email: 'bob@mail.com' }
];

深入探討

保留已移除元件的狀態

在真正的聊天應用程式中,當使用者再次選擇先前的收件者時,您可能希望恢復輸入狀態。有幾種方法可以讓不再可見的元件保持狀態「活動」

  • 您可以渲染所有聊天,而不僅僅是目前的聊天,但使用 CSS 隱藏所有其他聊天。聊天不會從樹狀結構中移除,因此它們的本地狀態將被保留。此解決方案非常適合簡單的 UI。但如果隱藏的樹狀結構很大且包含許多 DOM 節點,它可能會變得非常慢。
  • 您可以將狀態提升並在父元件中保存每個收件者的待處理訊息。這樣,當子元件被移除時,就無所謂了,因為是父元件保留了重要資訊。這是最常見的解決方案。
  • 除了 React 狀態之外,您還可以改用不同的來源。例如,即使使用者意外關閉頁面,您可能也希望訊息草稿能夠保留。要實現這一點,您可以讓 Chat 元件透過從 localStorage 讀取來初始化其狀態,並將草稿也儲存到那裡。

無論您選擇哪種策略,與Alice 的聊天在概念上都與與Bob 的聊天不同,因此根據目前的收件者為 <Chat> 樹狀結構提供一個 key 是有意義的。

重點回顧

  • 只要相同的元件在相同位置渲染,React 就會保留狀態。
  • 狀態不會保存在 JSX 標籤中。它與您放置該 JSX 的樹狀結構位置相關聯。
  • 您可以透過賦予子樹不同的 key 來強制其重置狀態。
  • 不要巢狀元件定義,否則您會意外重置狀態。

挑戰 1 5:
修復消失的輸入文字

這個範例在按下按鈕時會顯示訊息。然而,按下按鈕也會意外地重置輸入。為什麼會發生這種情況?請修復它,讓按下按鈕不會重置輸入文字。

import { useState } from 'react';

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

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