React - useState Hook 基本介紹

前言

本篇介紹 React 中 useState Hook 基本概念、語法和常見使用場景。文章詳細解釋了如何在函數組件中管理各種類型的狀態,包括簡單值、物件和陣列,並提供了實用範例如計數器、表單處理和條件渲染。同時探討了函數式更新的重要性及如何解決閉包問題。


什麼是 useState?

useState 是 React 中最基本的 Hook,它讓函數組件能夠擁有和管理狀態。在 React 16.8 版本引入 Hooks 之前,只有類組件才能擁有狀態,而函數組件被稱為「無狀態組件」。

有了 useState Hook,函數組件也能夠:

  • 保存數據(狀態)
  • 更新 UI
  • 記住用戶操作
  • 觸發重新渲染

基本語法

const [state, setState] = useState(initialValue)
  • state:目前的狀態值
  • setState:更新狀態的函數
  • initialValue:狀態的初始值

這種寫法使用了 JavaScript 的解構賦值語法。useState 返回一個包含兩個元素的陣列,第一個元素是狀態值,第二個元素是更新狀態的函數。


實用範例

基本計數器

最簡單的 useState 範例是一個計數器:

import React, { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<p>當前計數:{count}</p>
<button onClick={() => setCount(count - 1)}>減少</button>
<button onClick={() => setCount(0)}>重置</button>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}

當點擊按鈕時,我們呼叫 setCount 更新 count 的值,React 會使用新的狀態值重新渲染組件。

表單輸入

處理表單輸入是 useState 的另一個常見用例:

function TextInput() {
const [inputValue, setInputValue] = useState('');

return (
<div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="請輸入文字..."
/>
<p>您輸入的文字:{inputValue}</p>
</div>
);
}

在 React 中,表單元素使用「受控組件」模式,通過綁定 valueonChange 事件,使表單數據由 React 組件控制。這種方式讓我們能夠:

  • 立即對用戶輸入做出反應
  • 驗證輸入的值
  • 有條件地禁用表單提交按鈕
  • 強制特定輸入格式

顯示/隱藏內容

useState 也可以用來控制 UI 元素的顯示與隱藏:

function ToggleContent() {
const [isVisible, setIsVisible] = useState(true);

return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>
{isVisible ? '隱藏內容' : '顯示內容'}
</button>

{isVisible && (
<div>
<p>這是一段只有在 isVisible 為 true 時才會顯示的內容。</p>
<p>使用條件渲染是 React 中常見的模式。</p>
</div>
)}
</div>
);
}

使用布林值作為狀態,控制內容的顯示和隱藏,並通過條件渲染顯示或隱藏內容。這種模式常用於:

  • 模態對話框
  • 切換導航選單
  • 展開/摺疊內容
  • 切換暗/亮模式

使用物件作為狀態

當需要管理多個相關的狀態值時,可以使用物件:

function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: ''
});

const handleUserChange = (e) => {
const { name, value } = e.target;
setUser({
...user, // 保留其他欄位的值
[name]: value // 更新當前欄位
});
};

return (
<div>
<div>
<label>姓名:</label>
<input
type="text"
name="name"
value={user.name}
onChange={handleUserChange}
/>
</div>

<div>
<label>電子郵件:</label>
<input
type="email"
name="email"
value={user.email}
onChange={handleUserChange}
/>
</div>

<div>
<label>年齡:</label>
<input
type="number"
name="age"
value={user.age}
onChange={handleUserChange}
/>
</div>

<div>
<h3>使用者資訊:</h3>
<p>姓名:{user.name || '(未填寫)'}</p>
<p>電子郵件:{user.email || '(未填寫)'}</p>
<p>年齡:{user.age || '(未填寫)'}</p>
</div>
</div>
);
}

重要提示:使用物件作為狀態時,更新時必須保留未變更的欄位,React 不會自動合併狀態。

與 class 組件的 this.setState() 不同,useState 的更新函數不會自動合併更新物件。因此我們需要使用展開運算符 ...user 複製原始物件的所有屬性,再修改其中一個屬性。

使用陣列作為狀態

useState 也可以管理陣列,例如多選清單:

function CheckboxList() {
const [selectedItems, setSelectedItems] = useState([]);
const items = ['蘋果', '香蕉', '橘子', '葡萄', '西瓜'];

const toggleItem = (item) => {
if (selectedItems.includes(item)) {
// 如果項目已選中,則移除
setSelectedItems(selectedItems.filter(i => i !== item));
} else {
// 如果項目未選中,則添加
setSelectedItems([...selectedItems, item]);
}
};

return (
<div>
<h3>請選擇您喜歡的水果:</h3>
{items.map(item => (
<div key={item}>
<input
type="checkbox"
id={item}
checked={selectedItems.includes(item)}
onChange={() => toggleItem(item)}
/>
<label htmlFor={item}>{item}</label>
</div>
))}

<h3>已選擇的水果:</h3>
{selectedItems.length === 0 ? (
<p>(尚未選擇任何水果)</p>
) : (
<ul>
{selectedItems.map(item => (
<li key={item}>{item}</li>
))}
</ul>
)}
</div>
);
}

與物件狀態類似,更新陣列狀態時也不能直接修改原陣列,而是需要創建新的陣列。常用的陣列更新方法包括:

  • filter():過濾陣列項目
  • map():轉換陣列項目
  • concat():添加新項目
  • 展開運算符 [...array, newItem]:添加新項目

重要技巧:函數式更新

當新狀態依賴於之前的狀態時,建議使用函數式更新來避免閉包問題:

// 不建議的方式:可能導致閉包問題
setCount(count + 1);

// 推薦的方式:使用函數式更新
setCount(prevCount => prevCount + 1);

函數式更新可確保你總是使用最新的狀態值,尤其是在:

  • 快速連續更新狀態時
  • 異步操作中更新狀態時
  • 使用舊的 props 或 state 引用時

閉包問題與解決方案

閉包問題是 React 函數組件中常見的陷阱。讓我們通過一個例子來理解它:

function DelayedCounter() {
const [count, setCount] = useState(0);

// 有閉包問題的版本
function handleClickWithBug() {
setTimeout(() => {
// 這裡的 count 是點擊時的值,不是最新值
setCount(count + 1);
}, 2000);
}

// 解決閉包問題的版本
function handleClickFixed() {
setTimeout(() => {
// React 會將最新的 count 值傳給這個函數
setCount(prevCount => prevCount + 1);
}, 2000);
}

return (
<div>
<p>當前計數: {count}</p>
<button onClick={handleClickWithBug}>
增加計數(有問題的版本)
</button>
<button onClick={handleClickFixed}>
增加計數(修復版本)
</button>
</div>
);
}

閉包問題解釋
當你在事件處理函數中使用 count 變數時,JavaScript 會「捕獲」當時的值。如果你點擊按鈕後 count 的值是 0,然後在 setTimeout 的回調執行之前 count 變成了 5,回調中仍然會使用捕獲的值 0,而不是最新的值 5。

解決方案
使用函數式更新,React 會確保傳遞給你的函數的 prevCount 始終是最新的狀態值,而不是捕獲的舊值。


學習總結

  1. 基本概念:useState 讓函數組件能夠擁有和管理狀態。

  2. 工作原理:當使用 setState 更新狀態時,React 會重新渲染組件,但只更新變化的部分,保持高效。

  3. 常見用法

    • 使用簡單值(數字、字串、布林值)作為狀態
    • 使用物件作為狀態(需使用展開運算符保留其他欄位)
    • 使用陣列作為狀態(需創建新陣列而非修改原陣列)
    • 使用函數式更新避免閉包問題
  4. 最佳實踐

    • 狀態應該保持最小且必要
    • 相關狀態可以合併成一個物件
    • 需依賴之前狀態更新時,應使用函數式更新
    • 不要直接修改狀態,而是創建新的狀態值