前言
本篇介紹 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 中,表單元素使用「受控組件」模式,通過綁定 value 和 onChange 事件,使表單數據由 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(() => { setCount(count + 1); }, 2000); } function handleClickFixed() { setTimeout(() => { 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 始終是最新的狀態值,而不是捕獲的舊值。
學習總結
基本概念:useState 讓函數組件能夠擁有和管理狀態。
工作原理:當使用 setState 更新狀態時,React 會重新渲染組件,但只更新變化的部分,保持高效。
常見用法:
- 使用簡單值(數字、字串、布林值)作為狀態
- 使用物件作為狀態(需使用展開運算符保留其他欄位)
- 使用陣列作為狀態(需創建新陣列而非修改原陣列)
- 使用函數式更新避免閉包問題
最佳實踐:
- 狀態應該保持最小且必要
- 相關狀態可以合併成一個物件
- 需依賴之前狀態更新時,應使用函數式更新
- 不要直接修改狀態,而是創建新的狀態值