iT邦幫忙

2023 iThome 鐵人賽

DAY 21
0

昨天學了元件間如何共用 state,今天要來看如何保存和重置 state。

今天的內容:

  • UI 樹中的位置
  • 在相同的位置的相同元件
  • 在相同位置的不同元件
  • 在相同的位置重置狀態

UI 樹中的位置

當使用 React 時,透過元件建立一個虛擬的使用者介面(UI)樹,然後 React DOM 利用這個樹結構來呈現在瀏覽器中。在這個 UI 樹當中,元件的「位置」指的是元件在樹狀結構中的相對位置和巢狀關係,而這個位置的變化會直接影響到元件的狀態是否被保留,因為 React 會根據元件的位置來管理它們的狀態。

(出處 React 官方文件,中間的圖就是 UI 樹)

接著來看一些實際的例子來了解這個機制吧。

在相同的位置的相同元件

這是一個計數器,點擊「Add one」按鈕可以改變計數器的值使其增加。若使用者勾選了「Use fancy Styling」觸發重新渲染後,計數器的值會改變嗎?

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

答案是不會改變,因為Counter在 UI 樹當中的位置並未改變。對於 React 來說,它是在相同的位置上的相同元件。要特別注意的是當我們提到「位置」,它指的是 UI 樹當中的位置而不是 JSX markup 當中的位置。

在相同位置的不同元件

這次我們來看看如果是在相同位置,但不同元件的情況會是如何:在這邊有一個「Take a break」的勾選欄,當勾選計數器會消失,取而代之的是一個<p>

在這邊有一個「Take a break」的勾選欄,當勾選計數器會消失,取而代之的是一個<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>
  );
}

在這邊,當我們先點擊了計數器到 3 再勾選「Take a break」,當取消勾選再次看到計數器的時候,上面的值是多少呢?

Counter 和<p>在這邊都是同樣的位置,但答案是會歸 0,state 並不會被保存,原因是當我們勾選「Take a break」的時候,React 將Counter從 UI 樹當中移除(remove)並將 state 銷毀(destroy)了。而當我們在同樣的位置渲染不同的元件,它會重置整個子樹(subtree)的狀態

在相同的位置重置狀態

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

為什麼這邊即使切換了選手,分數卻沒有重新計算呢?

迅速回憶起剛剛學過的東西,因為兩個 Conters 都出現在同樣的位置,React 認為它們是 props 改變但依然是同樣的 Counter。所以並沒有重新記算分數。

那如果我們希望改變選手能夠重新計分該怎麼做呢?有二個作法

方法一:既然 React 認定這是同樣位置,那我們可以告訴 React 這是不同的位置

方法二:告訴 React 這二個 Counters 是不一樣的東西

1.在不同位置上渲染元件

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

透過改變 JSX 結構,可以讓 React 知道在同一個位置交替顯示不同的計數器元件。

如果 isPlayerAtrue,則呈現 「Taylor」 的計數器;如果 isPlayerAfalse,則呈現 「Sarah」 的計數器。

2. 透過 key

我們也可以透 key 來告訴 React,這二個元件是不同的東西。

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

當我們使用 key 時,可以讓 React 知道,這裡有二個不同的元件存在。因次在切換時,我們能夠達到 reset 計時器的目的。

小結

今天我們觀察了在 UI 樹位置中,元件的 state 的保存狀況,以及學習了如何去 reset。今天的內容官方文件也有非常清楚的示意圖幫助釐清這些觀念,可以參考看看。明天要進入 reducer function 的學習。

參考資料


上一篇
Day 20 - 在元件間共用 state
下一篇
Day 22 - Reducer 的應用
系列文
30 days of React 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言