跳至主要内容

useState()

什麼是 useState()

useState() 是一個內建的鉤子 (hook),用於在元件中宣告一個狀態 (state),他屬於響應式數值useState() 接收一個任意型別的參數作為狀態的初始值,並回傳含有兩個元素的陣列:狀態目前的數值以及用來更新該狀態的函式。例如:

import { useState } from 'react'

const [count, setCount] = useState(0)

在這個範例中,count 是一個狀態,初始值為 0setCount() 則是用來更新 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)

讓我們用一個簡單的 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(() => () => { ... });由於狀態初始化函式的關係,我們必須替他額外包裝一層函式。雖然這的確能運作,但是感覺起來好像不太對,對吧?

就如我們在響應式數值中所提到的,只有在數值會發生變化,而且使用者必須在畫面上觀察到他的變化時,我們才應該將其宣告為狀態。由於使用者不會在畫面上看見函式本身,因此我們不建議將函式宣告為狀態。在這種情況下,使用參考通常是較合適的選擇。