state 就像是一個快照(snapshot)
Rendering 指的是 React 呼叫元件 (元件是一個 function),return 的 JSX 就是當下畫面的快照,props、事件處理器、區域變數都會在那次 render 時被計算。而這個 UI snapshot 是可以互動的,React 會將 snapshot 和畫面做 match,並和事件處理器做連結
re-render 過程:
state 不像一般的變數,function return 後,state 是不會消失的!state 就像是獨立在 function 以外的空間,事件觸發 setter function,由 React 幫我們更新在獨立空間中的 state。
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
Setting state 只會在下一次的 render 做異動,即使連續呼叫三次 setNumber(number + 1)
結果也只會 + 1,因為在函式裡的 number 都是 0,這樣只是在告訴 React 下次 render,請幫我把現在的 number 值 + 1。
在同一次 render 過程中,state 值是固定的,即使在 setNumber(number + 1)
之後讀取 number,number 還是不會變的,因為他們用的是同一份 snapshot。即使是非同步的事件處理函式,React state 值都會維持不變!
React 會等到所有在事件處理函式都執行完成,才去處理 state 的更新。就像服務生不會客人每點一道菜就跑去廚房一次,會一次等客人都點完,確定最終內容才送給廚房。這樣可以確保不會執行太多次 re-render,也代表著直到事件處理函式都執行完成,UI 才會更新,也就是 React 的批次處理。
如果想要在下次 re-render 前,同一個狀態想要更新多次,可傳入函式給 setter function,例如:setNumer(n -=> n +1)
,用來告訴 React,請幫我利用 state 值執行一些程式碼,而不是單純的替換 state 值,例如:
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
}}>+3</button>
</>
)
}
這裡的 n => n+1
稱為 updater function,把它傳給 setter 時,React 會把它排進要執行的 queue 裡,當事件處理函式執行完畢,在下一次 render 時,React 會依序執行 queue 的內容,
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
當下一次 render 時,執行到 useState 時,React 會處理這些 queue,假設先前 number state 為 0,React 遇到第一個 n => n+1 時,會將 number 傳入,並 return 一個值,再接續傳給下一個 updater function,所以 useState 最後回傳 3。
如果在替換掉整個 state 數值之後,再去做 update ? 例如:
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>
setNumber(number + 5)
和 setNumber(n => n + 1)
會被加進 queue 中,再下一次 render 時,React 會依序執行 setNumber(number + 5)
和 setNumber(n => n + 1)
,最後存入 6。
總結來說,setter function 除了傳值,也可以傳入一個 updater function
不論是傳值或是傳函式,都會被放進 queue 中,等下一次元件 render 時,才會執行 queue。
state 可以是任何型式,在 JavaScript 中,數值、字串、boolean,屬於 primitive,state 可以從 0 改成 5,但數字 0 本身它是不會改變的。
而物件中的內容是可以改變的,稱為 mutation,例如:position.x = 5
即使物件屬於 mutation,但在 React 中,物件的操作盡量保持 immutable
,最好是整個取代物件,而不是只改物件中的值,對於物件,保持唯讀。
如果要做到深層複製,資料結構又很複雜,React 推薦使用 Immer 這套 library。useImmer(initialState)
和 useState
用法類似,不用使用 ...
展開物件,就跟直接更改物件屬性值的寫法一樣,Immer 會幫我們產生新的物件。
import { useImmer } from "use-immer";
const [person, updatePerson] = useImmer({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
updatePerson(draft => {
draft.artwork.city = 'Lagos';
});
跟物件一樣,React 中的 state 如果為陣列,應該是直接指派一個新的陣列,而不是直接改裡面的值。
建議使用 concat
、[...arr]
,filter
、slice
、map
這些陣列操作的方法。