第 5 章:State (狀態) 的介紹與使用
在上一章中,我們學習了 Props,它們允許父元件將資料傳遞給子元件。然而,Props 是唯讀的。如果一個元件需要管理其內部的、會隨時間或使用者互動而改變的資料,我們就需要使用 State。
State 代表了元件內部隨時間變化的資料。與 Props 不同,State 是由元件自己管理和控制的,並且當 State 改變時,React 會自動重新渲染元件以反映這些變化。
5.1 什麼是 State?
想像一個開關元件,它有兩種狀態:「開」和「關」。或者一個計數器元件,它需要記錄目前的計數值。這些會改變的值就是元件的 State。
- State 是私有的:它完全由元件自己控制。
- State 是可變的:與唯讀的 Props 不同,State 可以被修改。
- State 的改變會觸發重新渲染:這是 React 的核心機制之一。當你更新一個元件的 State 時,React 會安排該元件及其子元件的重新渲染。
5.2 Props vs. State
特性 | Props (屬性) | State (狀態) |
---|---|---|
資料來源 | 由父元件傳遞 | 由元件自身管理 |
可變性 | 唯讀 (子元件不能修改) | 可變 (元件可以修改自己的 State) |
生命週期 | 在元件的整個生命週期中通常保持不變 (除非父元件傳遞新的 Props) | 可以在元件的生命週期中改變,通常響應使用者互動或網路請求 |
用途 | 用於配置和自訂化元件的行為和外觀 | 用於儲存和管理元件內部的動態資料,控制元件的行為 |
誰擁有資料 | 父元件擁有資料,子元件接收資料 | 元件自身擁有和管理資料 |
簡單來說:Props 是元件的「設定」,而 State 是元件的「記憶體」。
5.3 在函式元件中使用 State:useState
Hook
在 React 16.8 版本之後,Hooks 的引入徹底改變了函式元件的編寫方式。useState
Hook 讓我們可以在函式元件中使用 State。
如何引入 useState
:
import React, { useState } from 'react';
useState
的基本語法:
useState
是一個函式,它接收一個參數作為 State 的初始值,並返回一個包含兩個元素的陣列:
- 目前 State 的值 (current state value)。
- 一個更新該 State 的函式 (updater function)。
const [stateVariable, setStateVariable] = useState(initialValue);
stateVariable
:你為 State 選擇的名稱。setStateVariable
:一個函式,通常命名為set
加上 State 變數的名稱 (例如setCount
,setName
)。呼叫此函式將更新 State 並觸發重新渲染。initialValue
:State 的初始值。它可以是任何 JavaScript 資料型別 (數字、字串、布林值、陣列、物件等)。這個初始值只在元件首次渲染時使用。
範例:一個簡單的計數器元件
// Counter.js
import React, { useState } from 'react';
function Counter() {
// 使用 useState Hook 來宣告一個名為 count 的 state 變數,初始值為 0
// count 是目前的 state 值
// setCount 是更新 count 的函式
const [count, setCount] = useState(0);
// 事件處理函式,用於增加計數
const handleIncrement = () => {
setCount(count + 1); // 更新 count 的值
};
// 事件處理函式,用於減少計數
const handleDecrement = () => {
setCount(count - 1); // 更新 count 的值
};
return (
<div>
<h2>計數器</h2>
<p>目前的計數: {count}</p>
<button onClick={handleIncrement}>增加 (+)</button>
<button onClick={handleDecrement}>減少 (-)</button>
<button onClick={() => setCount(0)}>重設</button> {/* 直接在 JSX 中使用箭頭函式更新 state */}
</div>
);
}
export default Counter;
在這個例子中:
useState(0)
初始化count
為0
。- 當使用者點擊「增加 (+)」按鈕時,
handleIncrement
函式被呼叫。 setCount(count + 1)
更新count
的值。React 會偵測到 State 的變化,並重新渲染Counter
元件,顯示新的計數值。
使用多個 State 變數:
一個函式元件可以擁有多個 State 變數,只需多次呼叫 useState
即可。
import React, { useState } from 'react';
function UserProfile() {
const [name, setName] = useState('訪客');
const [age, setAge] = useState(null);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const handleLogin = () => {
setIsLoggedIn(true);
setName('React 使用者');
setAge(3); // 假設 React 使用者 3 歲
};
const handleLogout = () => {
setIsLoggedIn(false);
setName('訪客');
setAge(null);
};
return (
<div>
<h2>使用者設定檔</h2>
{isLoggedIn ? (
<div>
<p>姓名: {name}</p>
<p>年齡: {age !== null ? age : '未知'}</p>
<button onClick={handleLogout}>登出</button>
</div>
) : (
<div>
<p>請先登入。</p>
<button onClick={handleLogin}>登入</button>
</div>
)}
</div>
);
}
export default UserProfile;
State 更新函式的行為 (Setter Function Behavior):
取代 State:當你使用
useState
的更新函式 (例如setCount
) 時,它會 取代 舊的 State 值,而不是像類別元件中的this.setState()
那樣進行合併 (特別是當 State 是物件時)。如果你希望保留物件中的其他部分,你需要手動合併。const [user, setUser] = useState({ name: 'Alice', age: 30 }); const updateUserName = (newName) => { // 如果 State 是物件,更新時需要手動合併 setUser(prevUser => ({ ...prevUser, name: newName })); };
函式更新 (Functional Updates):如果新的 State 需要基於先前的 State 計算得出,你可以向更新函式傳遞一個函式。這個函式會接收先前的 State 值作為參數,並返回新的 State 值。這在 State 更新依賴於先前值時非常有用,尤其是在處理非同步操作時,可以避免因為閉包導致的舊 State 值問題。
const handleIncrementFiveTimes = () => { for (let i = 0; i < 5; i++) { // 如果這樣寫,count 可能不會如預期增加 5 次,因為 setCount 是非同步的 // setCount(count + 1); // 使用函式更新可以確保每次都是基於最新的 state setCount(prevCount => prevCount + 1); } };
5.4 在類別元件中使用 State (簡介)
雖然現代 React 更推薦使用函式元件和 Hooks,但了解類別元件如何處理 State 仍然有助於理解舊有程式碼或某些特定場景。
在類別元件中,State 是一個物件,儲存在 this.state
中。
1. 初始化 State:
通常在建構函式 (constructor
) 中初始化 State。
import React from 'react';
class ClassCounter extends React.Component {
constructor(props) {
super(props); // 必須先呼叫 super(props)
// 初始化 state
this.state = {
count: 0,
message: "你好"
};
}
// ...
}
2. 讀取 State:
透過 this.state.propertyName
來讀取 State。
render() {
return (
<div>
<p>計數: {this.state.count}</p>
<p>訊息: {this.state.message}</p>
</div>
);
}
3. 更新 State:this.setState()
使用 this.setState()
方法來更新 State。this.setState()
會將傳入的物件與目前的 State 進行「淺合併」(shallow merge)。
handleIncrement = () => {
// 更新 count,message 保持不變
this.setState({ count: this.state.count + 1 });
};
handleChangeMessage = (newMessage) => {
this.setState({ message: newMessage });
};
this.setState()
的重要特性:
- 不要直接修改 State:永遠不要像
this.state.count = 10;
這樣直接修改 State,因為這不會觸發重新渲染,且可能導致非預期行為。 - State 更新可能是非同步的:React 可能會將多個
setState()
呼叫批次處理以提高效能。因此,你不應該依賴this.state
和this.props
來計算下一個 State。 函式更新:與
useState
類似,如果新的 State 依賴於先前的 State 或 Props,你可以向this.setState()
傳遞一個函式,而不是一個物件。這個函式會接收prevState
和prevProps
(或props
) 作為參數。this.setState((prevState, props) => ({ count: prevState.count + props.incrementStep }));
回呼函式:
this.setState()
接受一個可選的第二個參數,它是一個回呼函式,會在setState
完成並且元件重新渲染之後執行。this.setState({ count: 10 }, () => { console.log('State 更新完成,新的 count 是:', this.state.count); // 這裡會是 10 });
5.5 State 使用規則
無論是使用 useState
還是 this.setState
,都有一些重要的規則需要遵守:
不要直接修改 State (Do Not Modify State Directly)
- 對於
useState
:總是使用setXxx
函式來更新。 - 對於
this.state
:總是使用this.setState()
。 直接修改 State (例如count = 5;
或this.state.value = 5;
) 不會觸發重新渲染,並且可能在後續更新中被覆蓋。
- 對於
State 更新可能是非同步的 (State Updates May Be Asynchronous) React 為了效能考量,可能會將多次 State 更新批次處理。因此,如果你需要在更新後立即讀取 State 的值,它可能還不是最新的。
- 對於
useState
:如果新 State 依賴舊 State,使用函式更新形式setXxx(prevState => ...)
。 - 對於
this.state
:如果新 State 依賴舊 State,使用函式更新形式this.setState((prevState, props) => ...)
。如果需要在更新後執行操作,使用setState
的回呼函式。
- 對於
State 更新是合併的 (State Updates are Merged - 主要針對類別元件的物件 State)
- 當你使用
this.setState()
更新一個包含多個屬性的物件 State 時,React 會將你傳遞的物件與現有 State 進行淺合併。例如,如果this.state
是{ count: 0, theme: 'dark' }
,呼叫this.setState({ count: 1 })
會使this.state
變成{ count: 1, theme: 'dark' }
(theme 屬性被保留)。 - 對於
useState
,如果 State 是一個物件,更新函式 (如setUser
) 會 取代 整個物件。你需要自己處理合併邏輯,通常使用展開運算子 (...
):const [user, setUser] = useState({ name: 'A', role: 'admin' }); setUser(prevUser => ({ ...prevUser, name: 'B' })); // role 被保留 // setUser({ name: 'B' }); // 這樣會導致 role 消失
- 當你使用
5.6 何時使用 State?
當元件需要記住某些資訊,並且這些資訊會隨著時間或使用者互動而改變時,就應該使用 State。例如:
- 表單輸入的值
- 開關的開/關狀態
- 計時器的目前時間
- 從 API 獲取的資料
- 下拉式選單是否展開
- 目前選擇的項目
如果一個值不需要改變,或者可以從 Props 或其他 State 計算得出,那麼它可能不需要成為一個獨立的 State。
5.7 狀態提升 (Lifting State Up)
有時候,多個元件需要共享和操作相同的 State。在這種情況下,React 的常見模式是將共享的 State「提升」到它們最接近的共同父元件中。然後,父元件透過 Props 將 State 和修改 State 的函式傳遞給相關的子元件。
我們將在後續章節更詳細地探討這個重要的概念,因為它是構建複雜 React 應用的關鍵。
5.8 總結
State 使得我們的 React 元件能夠擁有「記憶」並對互動做出反應。
- State 是元件私有的、可變的資料。
- 在函式元件中,我們使用
useState
Hook 來管理 State。 useState
返回目前 State 和一個更新它的函式。- 在類別元件中,State 儲存在
this.state
中,並透過this.setState()
更新。 - 永遠不要直接修改 State;State 更新可能是非同步的。
- 當元件需要追蹤隨時間變化的資料時,使用 State。
- 「狀態提升」是一種在多個元件間共享 State 的重要模式。
掌握 Props 和 State 是理解 React 的核心。它們共同構成了 React 元件的資料管理基礎。在接下來的章節中,我們將學習更多關於元件生命週期和如何處理使用者事件的知識。