Skip to main content

useState

為何需要 state

今天我們先使用一個普通的 Javascript 變數 number 作為狀態值,當點擊畫面上的按鈕時,觸發事件時同樣會更新它的值:

//狀態變數
export default addButton = () =>{
const number = 0;
const addNumber = ()=>{number = number+1 }
return (
<>
<p> current {number} </p>
<button onClick={addNumber}></button>
</>
)
}

你會發現這個狀態值的變化無法被保存,因為每次重新渲染頁面時都會重新為 number 賦值。 react 提供的 useState 產生的狀態值,除了可以解決數值改變無法在頁面渲染間被保存的問題外,也state 改變與頁面渲染進行掛鉤

使用限制

  1. 只能在 function component 使用 ,class component 不能
  2. 只能在 function component 內的 top-level 呼叫,不可在 block scoped 呼叫

而所謂的 block scoped 舉例來說就包括:

import {useState} from 'react'

export default LimitShow = ()=>{
useState() //可
if(true) { useState(1) } // 不可
for(xx) { useState(1) } // 不可

return <></>
}
  1. 更新 state 需遵守 immutable 原則

在 Javascript 中資料主要分為兩種類型, Primitive typesReference types ,像是 Number,String,Boolean 等類型的資料,就是所謂的 Primitive types 的類型,而像是 Array , Object 這類型的資料則為 Reference types

如何區分兩者?

當你使用 const 進行宣告時,Primitive types 的資料無法再重新賦值,這就符合所謂的 immutable(不可改變)

const x = 2;
x = 3; //error x is constant variable

const y = x; //ok!

反之,你仍可對 Reference types 的資料進行調整,或是拷貝,這就是所謂的 mutable(可改變)

const objX = {name:"andy"};
objX.name = "danny" // ok!

為何 react 會嚴格限制 state 需遵守 immutable 原則? 作者理解 react 是透過偵測狀態(state)記憶體位置改變,來決定是否要重新渲染,當你直接對 Reference types 的資料進行調整時,記憶體位置並沒有改變,會發生資料已經改變,但是畫面沒有更新的情形。

回傳值

useState 會回傳一個陣列 ,index: 0 是初始的 state ,index: 1 則是更新 state 的函式。

function LimitShow(){
const state = useState(1);
state = [1,handlerState()]
}

所以一般使用上會搭配解構賦值將 儲存狀態變數處理狀態變數函式 解構,方便各別使用。

const [state,setState] = useState(1);

如何更新 state ?

Primitive types

const [state,setState] = useState(1)

//更新方式
setState(val)

Reference types

直接更新 reference type 的資料內容(如下圖),在 react 無法被偵測到 state 有產生改變,畫面更新會失效。

const [test,setTest] = useState({name:'1'})
// x: 直接更新物件 key 值
setState(()=>test.name="2")

要保持 state 值維持 immutable的情形去更新,有以下方法:

物件型別更新

離散語法

const [state,setState] = useState({x:1,y:2})

setState({...state,要更新部分的值})

存入全新物件

const [state,setState] = useState({x:1,y:2})
setState({x:2,y:3})

上面都是單層物件資料,如果今天是巢狀(nested)的狀態(如下),可以這樣思考去拆解

export default function nestObj(){
const [obj,setObj] =useState({
name:"andy",
interest:{ball:'basketball'}
})
}

  1. interest 物件先單獨拉出來
const newInterest = {...obj.interest}
newInterest.ball = 'baseball'
  1. 再將更新的值存入本來的狀態 interest 物件
setObj({...obj,interest:newInterest})

所以其實就是單層的更新方法做兩次。

陣列型別更新

陣列如同物件是 mutable 的,且許多內建的陣列方法會修改原始陣列造成 mutable ,這邊我擷取了 react 官方文件內條列的比較表(如下),透過這張表可以快速知道,要避免直接使用哪些方法更新 state 。

圖片來源:https://react.dev/learn/updating-arrays-in-state

加入新的資料

一般來說,可以使用 push (新增資料到最後一筆)、unshift (新增資料到第一筆),但這兩種都是 mutable 的方法,這邊同樣可使用離散語法替代

const [state,setState] = useState([x,y,z])
//把新資料放到最後一筆
seState([...state,newData])
//把新資料放在第一筆
setState([newData,...state])

將資料加入陣列中的特定位置

使用離散語法搭配 slice 就能實現

setState(...state.slice(0,1),newData)
tip

slice(a,b) 的兩個參數分別是指 a:從陣列中第幾個索引值開始 b:到哪個索引值停下(但不包含那個索引值)

如何使用 mutable 方法

其實上述圖表會造成 mutable 的方法,在 react 並不是不能使用,先釐清使用它們會產生的問題是 "影響本來的陣列",所以只要能避免這個情況,其實還是能使用的。

要不影響本來的陣列,常見的方法就是複製陣列,我們可透過離散語法完成這件事。

const [state,setState] = useState([1,2,3,4])

//離散複製一個
const sepArray = [...state]

//這時使用 mutable 的方法(反轉、排序)也沒差,因為 mutable 影響的也不是 state
const versedArray = sepArray.reverse()
const sortedArray = sepArray.sort()

但今天陣列內的資料若是 reference types 的 (如物件、陣列),即便複製了陣列仍會觸發 mutable 的問題,原因是離散語法是所謂的 shadow copy (淺拷貝),資料架構(如下)內部的物件其實沒有被複製到,仍然指向本來的位置。

const [state,setState] = useState([{name:"andy"}])

//只有複製外層陣列,內部物件還是指向相同記憶體
const copyState = [...state]
copyState[0].name = "danny" // 產生 mutable 問題

所以遇到這樣的資料類型,你會同時需要產生新的陣列及物件,官方文件的範例是使用用 map 搭配離散語法:

map 用來產生新的陣列,離散語法淺拷貝裡面的物件資料。

const [state,setState] = useState([{name:"andy"}])
const copyState = state.map((data)=>{return {...data}})

copyState[0].name = 'danny' // ok!

狀態更新機制

假設今天你實作了一個點擊功能,點擊按鈕後彈跳視窗會顯示更新後的狀態值(如下圖),但是當你實際點擊後,發現顯示在彈窗內的仍然是舊的數值,這邊就要帶到另一個重要觀念:State 更新是非同步的

export default AlertSection = ()=>{
const [message,setMessage] = useState(0)
const alertMessage = ()=>{
setMessage(2)
alert(`message is update! ${message}`)
}
return (
<>
<button onClick={alertMessage}>alert</button>
<p>{message}</p>
</>
)
}

再精確地來說:State 會等此次渲染的所有行為結束後才進行更新,意思就是說在當次觸發的行為內,你能讀取到的 State 都是舊的,

export default AlertSection = ()=>{
const [message,setMessage] = useState(0)
const alertMessage = ()=>{
setMessage(2)
alert(`message is update! ${message}`)
}
return (
<>
<button onClick={alertMessage}>alert</button>
<p>{message}</p>
</>
)
}

重看一次這個例子,之所以 alert() 出來的 message 仍然是 0 ,就是因為此次渲染的程序還沒結束, 所以 state 尚未更新,如果你希望能即時讀取到 state 的更新狀態, React 有提供 updater function 幫助你實現這件事。

updater function

所謂的 updater function 就是你將函式傳入 set 方法內,透過它去更新狀態值,

const [state,setState] = useState()

setState((n)=>n+1)

上述程式碼 (n)=>n+1 就是所謂的 updater function,上面有提過 state 數值更新,一般會在當次渲染程式都完成才更新,如果改用這個方法則不同,第一次呼叫 set 方法的回傳值會作為 parameter 被傳入第二次呼叫的,依序傳遞直到沒有更多 set 方法。

const [state,setState] = useState(0)

// directly update state
setState(state+1) // 0 + 1
setState(state+1) // 0 + 1
setState(state+1) // 0 + 1

//updater function
setState((n)=>n+1) // 0 + 1 = 1
setState((n)=>n+1) // 1 + 1 = 2
setState((n)=>n+1) // 2 + 1 = 3

batch update 機制

為何 batch update 要特別拉出一個章節,是因為自己理解它算是重要的性能節省機制。 前面有提過 state 改變與元件的 re-render 息息相關,假設今天事件觸發時,執行了多次的 set 方法 ,那照理來說應該會多次 re-render ,但實際上只會發生一次,這背後就是 batch update 機制在運作。

const [number,setNumber] = useState(0)
<button onClick={() => {
setCount(1);
setCount(2);
setCount(3);
}}>+3</button>

關於 batch update 的運作原理,我自己會用 Event loop 中的非同步概念來理解,當 set 方法觸發後,實際上並不會馬上執行,而是全部會放到類似 queueing 的等待列裡面,等全部內容都執行完開始一次執行。

圖片來源:https://ithelp.ithome.com.tw/articles/10300091

這樣的好處是避免無意義的渲染,造成多餘的效能損耗,我覺得官方文件的比喻很傳神:

假設每次狀態改變會觸發渲染這事件,想像為服務生為客人點餐,要是沒有 batch update 機制,你可以理解為客人每調整一次菜色,服務生就要跑一次廚房,這樣不是很沒效率?有了 batch update 機制,就像是客人確定這次要點的內容後,最終再一次去通知廚房上什麼菜。