useState()
什麼是 useState()
?
useState()
是一個內建的鉤子 (hook),用於在元件中宣告一個狀態 (state),他屬於響應式數值。useState()
接收一個任意型別的參數作為狀態的初始值,並回傳含有兩個元素的陣列:狀態目前的數值以及用來更新該狀態的函式。例如:
import { useState } from 'react'
const [count, setCount] = useState(0)
在這個範例中,count
是一個狀態,初始值為 0
;setCount()
則是用來更新 count
的函式。
這種語法被稱為解構賦值 (destructing assignment),用於將數值從物件或陣列中取出。若您不太理解這個概念,以下的虛擬碼 (pseudocode) 也許能幫助您理解 (請注意,這不是 setState()
的完整程式碼):
const useState = <T>(initialValue: T) => {
let currentValue: T = initialValue
const updateState = (value: T) => {
currentValue = value
}
return [currentValue, updateState]
}
由於您可以任意命名 useState()
回傳的元素,傳統上大家會用狀態來稱呼第一個元素 (數值),並用 setState()
來稱呼第二個元素 (函式)。
setState()
setState()
是一個用來更新狀態的函式。目前 setState()
有兩種使用方式:
- 傳遞一個數值,像是
setState(1)
和setState(count + 1)
。 - 傳遞一個函式,像是
setState((prev) => prev + 1)
。- 我們會等到更深入 React 之後才介紹這個方法,目前使用傳遞數值的方式就夠了!
讓我們用一個簡單的 counter app 當做例子:
import { useState } from 'react'
export const Example = () => {
const [count, setCount] = useState(0)
const increment = () => {
setCount(count + 1)
}
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>
Increment
</button>
</div>
)
}
在這個範例中,0
被用來當做 count
的初始值。每次 "Increment" 按鈕被點擊後,increment()
就會被呼叫,因此將 count
的數值更新為 count + 1
。
在 React 中,所有的狀態都應該經由對應的 setState()
函式來更新;不透過 setState()
直接更新狀態是個大問題!這是因為 setState()
旨在觸發元件的重新渲染,從而確保元件的狀態能反映在 UI 上。如果我們不使用 setState()
直接更新狀態,元件的 UI 可能就不會如預期的更新。
setState()
是異步的嗎?
您可能聽過有人說「setState()
是異步的 (asynchronous)」。這個說法有一部分是對的,因為 setState()
造成的改變並不會立即套用,但是 setState()
本身實際上是同步的;他並沒有回傳一個 promise。因此,對著他使用 await
是沒有必要的。
但是為什麼我們無法在 setState()
呼叫完成後立即拿到更新後的數值呢 (範例)?這是一個稍微複雜的概念,我們會等到更深入 React 之後再做更詳細的說明,目前先不用擔心他!
狀態初始化函式
若狀態初始值的運算比較複雜,有時候我們會想用一個函式來回傳這個值。舉例來說:
import { useState } from 'react'
const getSomething = () => {
// 做一些複雜的運算。
return something
}
export const Example = () => {
const [state, setState] = useState(getSomething())
return (
// ...
)
}
雖然範例中的寫法能正常運作,但是由於 JSX 運作機制的關係,getSomething()
實際上會隨著 Example
的重新渲染不斷的被呼叫。幸運的是,我們可以透過傳遞函式給 useState()
而不是傳遞數值來防止這種情況發生。例如:
const [state, setState] = useState(getSomething)
請注意,我們這次並沒有呼叫 getSomething()
;我們是將整個函式都傳給 useState()
,由他來替我們呼叫。但是,如果我們同時也想傳遞參數給 getSomething()
的話該怎麼辦呢?在這種情況下,我們可以替他額外包裝一層函式。例如:
import { useState } from 'react'
const getSomething = (value: number) => {
// 做一些複雜的運算。
return something
}
export const Example = () => {
const [state, setState] = useState(
() => getSomething(1)
)
return (
// ...
)
}
注意變數之間的相等性
在使用 setState()
更新一個非原始型別的狀態時,我們要特別注意變數之間的相等性。請看以下範例:
import { useState } from 'react'
export const Example = () => {
const [user, setUser] = useState({
name: 'hello',
})
const updateUser = () => {
setUser({
name: 'hello',
})
}
return (
<div>
<h1>User: {JSON.stringify(user)}</h1>
<button onClick={updateUser}>Update User</button>
</div>
)
}
在這個範例中,即使我們使用相同的值來更新 user
,元件仍然會重新渲染。這是因為被傳遞給 setUser()
的物件與我們用來初始化 user
的物件並不是同一個。
這個問題會發生在所有非原始型別的變數上,像是物件、陣列、map 等等。
什麼樣的數值適合被宣告為狀態?
即便 useState()
可以用來宣告任何型態的狀態,這不代表任何東西都適合作為狀態使用。舉例來說,我們可以用 useState()
來宣告一個函式型別的狀態,像是 useState(() => () => { ... })
;由於狀態初始化函式的關係,我們必須替他額外包裝一層函式。雖然這的確能運作,但是感覺起來好像不太對,對吧?
就如我們在響應式數值中所提到的,只有在數值會發生變化,而且使用者必須在畫面上觀察到他的變化時,我們才應該將其宣告為狀態。由於使用者不會在畫面上看見函式本身,因此我們不建議將函式宣告為狀態。在這種情況下,使用參考通常是較合適的選擇。