昨天學了元件間如何共用 state,今天要來看如何保存和重置 state。
今天的內容:
當使用 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 是不一樣的東西
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 知道在同一個位置交替顯示不同的計數器元件。
如果 isPlayerA
為 true
,則呈現 「Taylor」 的計數器;如果 isPlayerA
為 false
,則呈現 「Sarah」 的計數器。
我們也可以透 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 的學習。