跳至主要内容

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(() => () => { ... });由于状态初始化函数的关系,我们必须替他额外包装一层函数。虽然这的确能运作,但是感觉起来好像不太对,对吧?

就如我们在响应式数值中所提到的,只有在数值会发生变化,而且使用者必须在画面上观察到他的变化时,我们才应该将其声明为状态。由于使用者不会在画面上看见函数本身,因此我们不建议将函数声明为状态。在这种情况下,使用参考通常是较合适的选择。