React State 與 useState
React 元件透過資料狀態的改變來更新 UI,而元件有兩種資料來源:
- 透過外部傳進來元件的 Props
- 元件內部自己維護的 State
每當 React 偵測到 Props 或 State 有更新時,就會自動重新渲染(re-render)UI 元件。
Props 對於元件是唯讀(read-only)的資料,而 State 是元件可以自由讀寫的。
useState Hook
在 Function Component 中,我們使用 useState 這個 Hook 來建立和管理狀態。
基本語法:
import { useState } from 'react'
const [state, setState] = useState(initialValue)
state:目前的狀態值setState:更新狀態的函式initialValue:狀態的初始值
簡單範例:計數器
import { useState } from 'react'
function Counter() {
// 宣告一個 state 變數叫 count,初始值為 0
const [count, setCount] = useState(0)
return (
<div>
<p>你點擊了 {count} 次</p>
<button onClick={() => setCount(count + 1)}>點我</button>
</div>
)
}
這個範例中:
useState(0)建立一個初始值為 0 的狀態count是目前的計數值setCount是用來更新 count 的函式- 當按鈕被點擊時,呼叫
setCount(count + 1)將 count 加 1 - React 偵測到 state 改變,自動重新渲染元件
使用多個 State
你可以在一個元件中使用多個 useState:
function UserForm() {
const [name, setName] = useState('')
const [age, setAge] = useState(0)
const [email, setEmail] = useState('')
return (
<form>
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="姓名" />
<input
type="number"
value={age}
onChange={(e) => setAge(Number(e.target.value))}
placeholder="年齡"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
</form>
)
}
State 可以是各種資料型態
State 可以是任何 JavaScript 資料型態:
// 數字
const [count, setCount] = useState(0)
// 字串
const [name, setName] = useState('')
// 布林值
const [isOpen, setIsOpen] = useState(false)
// 陣列
const [items, setItems] = useState([])
// 物件
const [user, setUser] = useState({ name: '', age: 0 })
// null
const [data, setData] = useState(null)
更新物件和陣列 State
當 state 是物件或陣列時,你不能直接修改它們,而是要建立新的物件或陣列:
更新物件
const [user, setUser] = useState({ name: 'Mike', age: 25 })
// ❌ 錯誤:直接修改 state
user.age = 26
// ✅ 正確:建立新物件
setUser({ ...user, age: 26 })
// ✅ 也可以這樣寫
setUser((prevUser) => ({ ...prevUser, age: 26 }))
更新陣列
const [items, setItems] = useState(['apple', 'banana'])
// ❌ 錯誤:直接修改 state
items.push('orange')
// ✅ 正確:建立新陣列
setItems([...items, 'orange'])
// ✅ 移除項目
setItems(items.filter((item) => item !== 'banana'))
// ✅ 更新特定項目
setItems(items.map((item) => (item === 'apple' ? 'red apple' : item)))
State 更新是非同步的
React 為了效能考量,會將多個 setState 呼叫合併成一次更新(batching)。這意味著 state 的更新是非同步的,你不能在呼叫 setState 後立即拿到新的值:
function Counter() {
const [count, setCount] = useState(0)
function handleClick() {
setCount(count + 1)
console.log(count) // 還是舊的值!
}
return <button onClick={handleClick}>Count: {count}</button>
}
使用函式更新器
當新的 state 需要依賴前一個 state 時,應該使用函式更新器(functional update):
function Counter() {
const [count, setCount] = useState(0)
function handleClick() {
// ❌ 可能有問題:如果連續點擊太快
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
// count 可能只會 +1,而不是 +3
}
function handleClickCorrect() {
// ✅ 正確:使用函式更新器
setCount((prev) => prev + 1)
setCount((prev) => prev + 1)
setCount((prev) => prev + 1)
// count 會正確地 +3
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClickCorrect}>+3</button>
</div>
)
}
函式更新器接收前一個 state 值作為參數,回傳新的 state 值。這確保了即使在批次更新中,每次更新都是基於最新的 state。
Lazy Initialization
如果初始 state 需要經過複雜計算,你可以傳入一個函式,這個函式只會在初次渲染時執行:
// ❌ 每次渲染都會執行 computeExpensiveValue
const [state, setState] = useState(computeExpensiveValue())
// ✅ 只在初次渲染執行
const [state, setState] = useState(() => computeExpensiveValue())
實際範例:
function TodoList() {
// 從 localStorage 讀取初始資料,只在初次渲染時執行
const [todos, setTodos] = useState(() => {
const saved = localStorage.getItem('todos')
return saved ? JSON.parse(saved) : []
})
// ...
}
State 設計原則
1. 避免重複的 State
不要儲存可以從其他 state 或 props 計算出來的值:
// ❌ 不好:fullName 可以從 firstName 和 lastName 計算出來
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [fullName, setFullName] = useState('')
// ✅ 好:fullName 直接計算
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const fullName = `${firstName} ${lastName}`
2. 避免巢狀過深的 State
盡量保持 state 結構扁平化:
// ❌ 巢狀太深,更新困難
const [state, setState] = useState({
user: {
profile: {
name: 'Mike',
address: {
city: 'Taipei',
},
},
},
})
// ✅ 較扁平的結構
const [userName, setUserName] = useState('Mike')
const [userCity, setUserCity] = useState('Taipei')
3. 將相關的 State 分組
如果多個 state 總是一起更新,考慮合併成一個物件:
// 如果 x 和 y 總是一起變動
const [position, setPosition] = useState({ x: 0, y: 0 })
// 而不是
const [x, setX] = useState(0)
const [y, setY] = useState(0)
完整範例:待辦清單
import { useState } from 'react'
function TodoApp() {
const [todos, setTodos] = useState([])
const [inputValue, setInputValue] = useState('')
// 新增待辦事項
function addTodo() {
if (inputValue.trim() === '') return
setTodos([
...todos,
{
id: Date.now(),
text: inputValue,
completed: false,
},
])
setInputValue('')
}
// 切換完成狀態
function toggleTodo(id) {
setTodos(todos.map((todo) => (todo.id === id ? { ...todo, completed: !todo.completed } : todo)))
}
// 刪除待辦事項
function deleteTodo(id) {
setTodos(todos.filter((todo) => todo.id !== id))
}
return (
<div>
<h1>待辦清單</h1>
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="輸入待辦事項"
/>
<button onClick={addTodo}>新增</button>
</div>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
}}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>刪除</button>
</li>
))}
</ul>
</div>
)
}