一、淺談一下 props 和 state 的差異

比較對象 props state
使用範圍 可傳遞給子元件等。 用在單一元件本身,可以把它想做是元件自己所擁有的。如果 A, B 元件都有名為 count 的 state 也沒問題,因為不會互相干擾。
變更後是否更新畫面

二、基本運作原理

1. 基本宣告:

第一個值是初始內容,第二個值是方法,用來直接更新對應的初始內容後並觸發局部的重新渲染。

1
const [age, setAge] = useState(17);

2. 撰寫需要使用該 state 的元件:

以 function 應用為例。如果是在某些動作觸發等條件下才呼叫,可以直接調用 useState 的方法。
如果你想轉大人,可以有兩種做法:

1
2
const atAdd = () => { setAge(18); }; // 直接設定值
const atAdd = () => { setAge(age + 1); }; // 或根據上一個狀態計算成新值

以 jsx 應用為例。如果要在畫面上即時拿到更新後的state值,就是像拿變數一樣即可取得。

1
<div>目前年齡為:{age}</div>

三、state 的更新依據

原則上 state 值可以放入 number, string, boolean 等型別的資料,但因為 useState 的設計是透過 Object.is 的方法比較,所以當該 state 的新舊值相同,或者參考資料的記憶體位置沒有變更,React 就會基於效能考量,跳過重新渲染的動作。

所以當我們使用 state 來處理 array 和 object 型別的資料時就要特別留意。這樣說可能很模糊,可以看下方舉例:

範例1:原本我只有兩個朋友,想新增第三個時,第一個想法就是用array.push,但這樣並不會更新,因為是在原本的記憶體中改變資料,所以不會觸發 state 的變更。

1
2
3
const [friends, setFriends] = useState(['Annie', 'Lucy']);

setFriends(items.push('Sandy'));

範例2:因為用範例1的方式,我的朋友沒有因此增加,看了文件後了解需要使用展開運算子更新該 array,這時候就會更新啦!(終於不再三缺一)

1
2
3
const [friends, setFriends] = useState(['Annie', 'Lucy']);

setFriends([...items, 'Sandy']);

範例3:針對單筆資料時,可以用 Object.assign 方法,因為它是淺層複製的概念,對記憶體來說已經不同,所以會更新!

1
2
3
const [member, setMember] = useState({name: 'Annie', age: 18});

setMember({...member, city: 'Taipei'});

四、setState 有非同步的問題

官網中有這一段說明

Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. In the rare case that you need to force the DOM update to be applied synchronously, you may wrap it in flushSync, but this may hurt performance.

總之 useState() 是一個請求,而非用來立即更新,因為採取批次狀態更新,React 會在執行完函式後,調用 state 值並依照上一個段落所提到的更新依據(比對 state 的新舊值、或參考資料的記憶體位置沒有變更),有變更才會更新畫面。所以不論是直接設定值給 state 或根據上一個狀態計算成新值,都不會在當下拿到最新的值。

範例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default function Name() {
const [name, setName] = useState('Annie');

function atClick = () => {
setName('Andy');
setName('Ben');
setName('Cathy');
console.log(name); // 在函式運作結束前,都不會更新。會印出 Annie
};

return (
<button onClick={atClick}>
change {name} ! // 會印出 Andy
</button>
);
};

範例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default function Age() {
const [age, setAge] = useState(17);

function atClick = () => {
setAge(age + 1);
setAge(age + 2);
setAge(age + 3);
console.log(age); // 在函式運作結束前,都不會更新。所以這時候仍會印出 17
};

return (
<button onClick={atClick}>
change {age} ! // 會印出 18
</button>
);
};

為了解決這個問題,React 本身有提供 setState 的 callback 函式,它可以獲取待處理的值並從中更新下一個值。

範例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default function Age() {
const [age, setAge] = useState(17);

function atClick = () => {
setAge(prevAge => prevAge + 1); // 17 => 18
setAge(prevAge => prevAge + 2); // 18 => 20
setAge(prevAge => prevAge + 3); // 20 => 23
console.log(age); // 會印出 23
};

return (
<button onClick={atClick}>
change {age} ! // 會印出 23
</button>
);
};

五、學習心得

一開始學寫 React 時,第一個學的 hook 就是 useState,我會用它來做為樣式變更的依據值(像是切換主題或語言時的變化)、或者畫面上值的渲染等,確實會常常遇到非即時更新而造成的問題。今天深讀官方文件後整理了思緒,了解觸發更新的條件後,後續開發上也會知道如何避開原有的問題點,也希望這篇文章可以幫其他有困惑的夥伴們解惑~