一個應用程式最重要的功能,莫過於狀態管理。
在 React 內建的 Hook 中,也提供了兩種管理狀態的工具—— useState() 及 useReducer(),而當中較為常用的,就是本文將要介紹的 useState()。
何謂「狀態 (State)」?
正如 React 名稱所示,因應使用者的互動,React 元件也經常需要更新新的介面。比如說,
- 點擊「購買」按鈕,商品應該加入購物車
- 在搜尋框鍵入關鍵字,應該呈現篩選結果
為了做到這些事,React 元件需要「記住」某些當前的資料,並根據事件更新畫面。這種會隨事件改變、且影響畫面呈現的資料,在 React 中稱為狀態 (state)。
React Hooks 使用規範
在介紹 useState() 之前,先來瞭解 React Hooks 的基本規範:
1. 只能用在函式元件與自訂 Hooks 中
Hooks 僅能用於函式元件 (Function Component) 及自訂 Hooks 之中,無法使用於類別元件 (Class Component)。
在使用 Next.js 時,只有 Client Component 才能使用像 useState 這類需要狀態的 Hooks。
2. Hooks 必須依固定順序執行
每次元件渲染時,Hooks 都必須依照相同順序被呼叫。因此,Hooks 只能放在元件最上層,不能寫在:
- 條件語句,例如:
if / else - 迴圈,例如:
for、while - 一般函式中
這是 React 能正確對應狀態位置的前提。
useState() 基本用法
useState() 是 React 初始化狀態的基本方式:
首先,從 React 匯入 useState,並在函式元件中使用它。它接受一個參數,作為狀態的初始值:
import { useState } from "react"; // 匯入
const initialState = "初始狀態"
function App() {
useState(initialState);
return ...;
};
export default App;
useState() 會回傳一個長度為 2 的陣列,陣列的值依序是「當前的狀態」、「更新狀態的方法」(setter):
const stateArray = useState(initialState)
const state = stateArray[0];
const setState = stateArray[1];
不過這種寫法過於冗長,實務上幾乎不會這樣使用,在使用 useState() 時,通常會以陣列解構的形式出現:
const [state, setState] = useState(initialState);
各部分的意義如下:
- useState:定義元件狀態的 Hook
- initialState:狀態的起始值
- state:目前狀態的值
- setState:更新狀態的方法(會觸發重新渲染)
用 useState 實作計數器元件
先從一個靜態的元件開始:
function Counter() {
return (
<>
<button>-</button>
<div>0</div>
<button>+</button>
</>
);
};
export default Counter;
接著加入狀態,讓數字可以動態改變。
1. 加入狀態並顯示
import { useState } from "react"; // 1. 匯入 useState
function Counter() {
const [count, setCount] = useState(0)
// 2. 透過 useState 取得 count 的狀態及設定方法,
// 並以 0 作為初始值
return (
<>
<button>-</button>
{/* 3. 👇 改用 count 來呈現當前數字 */}
<div>{ count }</div>
<button>+</button>
</>
);
};
export default Counter;
2. 加上按鈕功能
// 減法功能
function decreaseCount() {
setCount(count - 1)
};
// 加法功能
function increaseCount() {
setCount(count + 1)
};
並分別綁定到對應按鈕:
// 減法按鈕
<button onClick={decreaseCount}>-</button>
// 加法按鈕
<button onClick={increaseCount}>+</button>
然後,將以上邏輯放入整個元件:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0)
function decreaseCount(){
setCount(count - 1);
};
function increaseCount(){
setCount(count + 1);
};
return (
<>
<button onClick={decreaseCount}>-</button>
<div>{ count }</div>
<button onClick={increaseCount}>+</button>
</>
);
};
export default Counter;
至此,一個最基本的計數器就完成了 🎉
使用函式設定初始狀態 (Initializer)
除了直接傳值之外,也可以傳入函式來設定初始狀態:
function initializeCount() {
console.log("count initialized");
return 0;
}
function Counter() {
const [count, setCount] = useState(initializeCount);
//...
}
這種函式稱為 initializer function,特點是:
- 必須是無參數的純函式
- 只會在初始渲染時執行一次
- 適合用於需要計算或轉換的初始值
但值得注意的是,傳入 useState 時並不需要呼叫,否則每次渲染都會執行:
// ❌ 每次渲染都會執行
useState(initializeCount());
為什麼不能用一般變數取代狀態?
從上方例子來看,React 的「狀態」似乎與原生 JS 的變數相去不遠。那麼,如果改用變數來存取 count,會發生什麼事?(僅用加法為例)
function Counter() {
let count = 0; // 改用一般變數定義
// 加法邏輯
function increaseCount() {
count = count + 1;
console.log(count);
}
return (
<>
<div>{ count }</div>
<button onClick={increaseCount}>+</button>
</>
);
}
export default Counter;
按下按鈕後,畫面並不會更新,但如果透過 console,會發現 count 的值確是會遞加的。
既然如此,在刻意用一般變數定義 count 的前提下,使用其他方式觸發重新渲染,讓我們看看這樣能否達成畫面的更新?為此,我們將在程式碼作出以下調整:
- 用 useState 設定狀態
renderTimes來觸發重新渲染 - 在渲染前後加上 console 觀察
count的變化
function Counter() {
let count = 0;
const [renderTimes, setRenderTimes] = useState(1);
// 👆 用 useState 給的 setState 方法作為觸發元件重新渲染的方式
console.log("渲染次數:", renderTimes);
console.log("計算前的數字:", count);
function increaseCount() {
count = count + 1;
setRenderTimes(renderTimes + 1); // 觸發元件重新渲染
console.log("計算後的數字:", count);
}
return (
<>
<div>{ count }</div>
<button onClick={increaseCount}>+</button>
</>
);
}
export default Counter;
這次,畫面依然沒有更新,查看 console,會是這樣的情形:
渲染次數:1
計算前的數字:0
計算後的數字:1
渲染次數:2
計算前的數字:0
計算後的數字:1
藉由以上兩種實驗,除了應證上述狀態可以跨渲染保留之外,對於一般變數,我們可以得到兩個結論:
- 一般變數的改變 → 不會觸發重新渲染
- 元件重新渲染後 → 一般變數會重新計算
使用一般變數的狀況:狀態的衍生值 (derived value)
當然,並非一昧地使用狀態定義變數就好,由於 React 的運作機制,當使用 setState 時,會發生以下流程:
- 標記狀態發生改變的元件
- 重新執行該函式元件
- 重新計算 JSX
- 比對 Virtual DOM
- 視情況更新真實 DOM
一次重新渲染是昂貴的,相對的,一般變數不會被 React 追蹤、不會觸發重新渲染;若以效能角度來思考,如果需要的資料是狀態的衍生值 (derived value) 的話,使用一般變數制定會是更好的實踐:
function Counter() {
const [count, setCount] = useState(0);
const doubleCount = count * 2;
// doubleCount 是由 count 衍生出來的值,用一般變數定義即可
// ...
}
重新渲染不是在更新狀態後同步執行的
假如現在希望按下增加按鈕時,讓當前數字加 2,而連續呼叫 setCount(count + 1) 兩次:
function increaseCount(){
setCount(count + 1);
setCount(count + 1);
};
即使如此,畫面只會加 1。
為了避免在單一事件處理中反覆渲染元件多次,因此 React 是在事件處理結束後,對狀態進行批次處理 (batching)。
可以想像每次渲染都是一張固定的快照,當批次處理狀態時,React 才會比對差異,決定下次渲染的部分。因此,在事件處理中直接讀取狀態時,取得的都會是「當次渲染的快照」,而不是即將更新後的值。
因此在處理 increaseCount 時,在函式所讀取到的 count,都會是當前的狀態。
使用 updater function 更新狀態
若更新狀態時需要依賴「上一個即將更新的狀態」,可以使用 updater function:
setCount(c => c + 1);
Updater 作為 setState 的回調函式,需以上個待更新的狀態作為參數,回傳處理過後的結果。
例如:
const [count, setCount] = useState(5);
function increaseCount(){
setCount(count + 1);
setCount(c => c + 1);
};
此時第二個 setCount 會基於「上一個 setCount 的待更新狀態」,也就是 count + 1 的結果,便能夠在下次渲染看到正確累加的結果。
物件與陣列的狀態更新
物件及陣列在 JavaScript 中都屬於可變資料,可以透過指派或一些原生方法來直接改變其內容;但 React 處理狀態的方式,其實比較接近不可變資料的處理手法,也就是以新的狀態覆寫舊的狀態。
比如說,我們透過 useState 來定義 user 的資料:
const [user, setUser] = useState({
id: 7,
name: "Leon",
age: 18,
});
❌ 錯誤做法:
user.age = 19;
setUser(user); // 未建立新參考,不會觸發渲染
setUser({ age: 19 }); // 下次渲染將以 { age: 19 } 覆蓋整個物件
✅ 正確做法:
setUser({
...user, // 1. 複製原本的物件
age: 19; // 2. 覆寫更新的鍵值
});
陣列資料也同理,比如說:
const [nums, setNums] = useState([1, 2, 3]);
❌ 錯誤做法:
nums.push(4);
setNums(nums); // 未建立新參考
✅ 正確做法:
setNums([...nums, 4]);
透過 setState 設定物件資料(物件、陣列),基本上要注意兩大點:
- 建立新的參考,React 才能正確判斷狀態改變。
- 傳入要覆寫舊狀態的完整物件。
透過 key 重置狀態
最後,補充一個應該算是相對冷門的知識,我們可以透過變更元件的 key 值,來重置該元件的狀態。
比如說,
function App() {
const [version, setVersion] = useState(0);
function handleReset() {
setVersion((v) => v + 1);
}
return (
<>
<div className="flex flex-column">
<Counter key={version} />
<button onClick={handleReset}>Reset</button>
</div>
</>
);
}
CodePen 範例:
React 狀態是屬於元件實例的,當渲染發生時,React 會嘗試重用舊的元件實例來保留狀態。至於如何判斷是否屬於同一個元件,大致會根據以下順序:
- 元件名稱(
<Counter />vs.<User />) - key 值
其中一個不同,就會被視為不同元件,當被視為不同的元件,React 就會創造新的實例,卸載舊的元件,讓新的元件掛載,狀態也會被初始化。
換句話說,key 值像是身分證,協助 React 辨識元件的身份,這也是為什麼當渲染列表時,會需要使用不同的 key。當列表項目發生變動,或是某一項目的狀態改變時,React 能夠保留未變動的元件狀態,進而減少效能的浪費。
結論
- 狀態更新能夠在跨渲染保留;一般變數則無法
- 狀態更新不會同步反映在當次渲染中
- 當狀態的初始值需要昂貴計算時,可用 initializer 初始化
- 狀態的衍生值建議透過一般變數制定
- 變更狀態需要依賴前一個狀態時,請使用 updater function
- 物件與陣列狀態更新時,務必建立新的參考
- 改變 key 值可以用來重置元件的狀態
參考
- React 官方文件
- Learn useState In 15 Minutes - React Hooks Explained
- Everything You Need To Know About useState