useState 入門:讓元件擁有自己的資料
更新日期: 2025 年 4 月 16 日
在學會 React 元件後,你可能會發現一個問題:當畫面重新渲染時,元件內的變數好像會「歸零」?
舉個例子,如果你寫了一個 let count = 0
,但當使用者點按按鈕後畫面更新,count
還是從 0 開始。
那該怎麼辦呢?這就是 React 提供的第一個 Hook —— useState
派上用場的地方!
什麼是 state?為什麼我們需要它?
state 是什麼?
在 React 中,state(狀態)是一種能讓元件「記住資料」的機制。
當你寫一個按鈕、輸入框或其他互動元件時,你會希望使用者的動作能反映在畫面上,這時就需要「記住」某些資料 —— 而這些資料,就是 state。
可以這樣想:
State 就像是一個元件的「內部記憶」,記錄了現在的狀態是什麼,讓元件知道該怎麼顯示畫面。
✅ 舉例來說:
- 使用者點了一下「+1」按鈕,畫面中的數字要加一
➤count
是一個 state,記錄目前的數字 - 使用者切換了燈泡的開關,畫面要顯示開或關的圖示
➤isOn
是一個 state,記錄燈泡的狀態 - 使用者在表單輸入名字,你要即時顯示他輸入的內容
➤name
是一個 state,記錄使用者輸入的值
為什麼不能用一般變數?
剛開始學 React 時,你可能會想用 JavaScript 的方式寫:
let count = 0;
function MyComponent() {
function handleClick() {
count += 1;
console.log(count);
}
return <button onClick={handleClick}>點我</button>;
}
乍看之下好像可以,但你會發現 畫面不會更新,即使 count
的值改變了。
這是因為:
React 是「聲明式」的 UI 框架,只有透過它的狀態機制(像
useState
)變更資料,React 才知道「要重新渲染畫面」。
🧾 什麼是「聲明式(Declarative)」的 UI 框架?
✅ 定義:你只需要告訴系統「要呈現什麼」,而不是「怎麼一步步去做到」
👀 比喻說明:像是點菜 vs 自己下廚
- 聲明式(Declarative):你去餐廳點一份牛排
→ 你只需要「描述想要的結果」(我想吃五分熟牛排配黑胡椒醬)
→ 廚師會幫你搞定怎麼做、用什麼順序、開多大火、什麼時候翻面 - 命令式(Imperative):你自己下廚做牛排
→ 你必須自己「一步步告訴系統該做什麼」
→ 拿鍋子、開火、熱油、放牛排、煎 3 分鐘翻面、撒調味料…
🖼 在 UI 編程中呢?
🟥 傳統命令式框架(例如 jQuery)
你會寫出一堆這樣的流程控制語句:
const list = document.createElement('ul');
for (let i = 0; i < items.length; i++) {
const li = document.createElement('li');
li.textContent = items[i];
list.appendChild(li);
}
container.appendChild(list);
👉 你得親自「一步步告訴電腦」:
- 怎麼建立元素
- 加入到哪個節點
- 寫入什麼資料
🟩 React 的聲明式寫法
React 則讓你直接描述畫面「長什麼樣」就好:
function ItemList({ items }) {
return (
<ul>
{items.map((item) => <li key={item}>{item}</li>)}
</ul>
);
}
👉 重點是:
你不需要關心「要建立多少個 <li>
元素」、「要用哪個 DOM 方法 append」
你只需要說:「我想要一個清單,裡面有這些項目」
React 就會自動幫你比對差異、更新畫面
🎯 為什麼這樣比較好?
- ✅ 程式碼更簡潔、語意更清楚
- ✅ 不容易出錯(不用手動操作 DOM)
- ✅ 更容易維護和修改(改 UI 結構就像改 HTML 一樣直覺)
- ✅ React 幫你處理底層複雜的流程(虛擬 DOM、重新渲染邏輯)
🧠 小結:聲明式 vs 命令式
觀念 | 命令式(Imperative) | 聲明式(Declarative) |
---|---|---|
你要做什麼? | 一步步告訴電腦怎麼做事情 | 描述你想要的結果,讓框架幫你處理 |
程式風格 | 流程控制、多行邏輯、手動更新 | 結構清晰、描述式、框架自動處理 |
實作方式 | 操作 DOM、自己追蹤狀態、管理節點 | 使用 state + JSX 描述畫面 |
React 屬於哪種? | ❌ 否 | ✅ 是 |
React 的設計理念正是:
把 UI 當成「狀態的函數」來寫,而不是一步步「組出來」的結果
如果你有用過 Excel,也可以這樣理解:
- 在 Excel 裡,
=A1 + B1
是不是自動根據資料改變?
➤ 這就是一種聲明式的邏輯。
✅ 具體來說,問題有三個:
- 一般變數不會觸發重新渲染
當你改變let count = 0
這種變數的值,React 並不知道要重新渲染元件,因此畫面不會更新。 - 變數每次渲染都會重設
在函式型元件中,每次畫面重新渲染,整個函式會重新執行一次。
所以let count = 0
這樣的寫法,每次都會從 0 開始,相當於「失憶」。 - 資料狀態與畫面不同步
UI 的畫面應該反映目前的資料狀態,但如果你用的是外部變數、全域變數或 function 內部的變數,很容易造成資料與畫面「不同步」的 bug。
✅ 用 useState 就對了!
為了解決這些問題,React 提供了 useState()
這個 Hook:
const [count, setCount] = useState(0);
這樣 React 就能:
- 在畫面中保留
count
的值(即使重新渲染也不會被洗掉) - 當你呼叫
setCount()
時,自動更新畫面
這也是為什麼 React 建議你用 useState
管理資料,而不是用普通的變數。
🧠 小總結
方式 | 會記住資料嗎? | 會觸發畫面更新嗎? | 建議用法 |
---|---|---|---|
let count = 0 | ❌ 每次都被重設 | ❌ 不會更新畫面 | ❌ 不建議 |
useState(0) | ✅ 會記住資料 | ✅ 自動更新畫面 | ✅ 正確做法 |
useState 的基本用法
當你希望 React 元件能記住某個資料,並在資料改變時自動更新畫面,就需要使用 useState()
這個 Hook。
基本語法與結構
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>目前計數:{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
✅ 解說:
語法部分 | 解釋 |
---|---|
useState(0) | 這是初始化狀態,初始值是 0,也可以是字串、布林值、物件或陣列等任何類型 |
[count, setCount] | 透過「解構賦值」拿到兩個東西:count 是目前的值,setCount 是修改它的函式 |
setCount(count + 1) | 呼叫這個函式會「更新狀態」,並自動觸發畫面重新渲染 |
onClick={() => setCount(...)} | 當按鈕被點擊時,就會執行這段更新狀態的邏輯 |
React 每次重新 render 時,會根據最新的 state 來更新畫面,而不需要你手動操作 DOM。
❗ 小提醒:不要直接改變 state!
你可能會直覺地寫:
count = count + 1; // ❌ 錯誤!這樣不會讓 React 重新渲染畫面
這樣的寫法只是在函式內部改變變數,React 完全不知道發生了什麼事。
✅ 正確做法是:
setCount(count + 1); // ✅ React 會接收到這次變動,觸發畫面更新
因為 React 是透過 setCount()
來追蹤狀態改變並重新 render 的,如果你繞過它去改變值,React 就不會理你。
延伸閱讀:React 核心觀念解密:setCount 究竟是什麼?為何它能同時接收數值與函式?
多個 state 該怎麼寫?
在一個元件中,我們常常不只需要一筆資料。
例如一個燈泡元件,可能要同時記住「有沒有開」跟「顏色是什麼」。
那我們該怎麼做?
你可以宣告多個 useState
,每筆狀態獨立管理,各自更新,非常靈活。
function LightSwitch() {
const [isOn, setIsOn] = useState(false); // 控制燈泡開關
const [color, setColor] = useState('yellow'); // 控制燈泡顏色
return (
<div>
<p>狀態:{isOn ? '💡 開' : '🌑 關'},顏色:{color}</p>
<button onClick={() => setIsOn(!isOn)}>切換開關</button>
<button onClick={() => setColor('blue')}>變藍色</button>
</div>
);
}
✅ 為什麼不用一個 object 管理多個 state?
你當然可以這樣寫:
const [light, setLight] = useState({ isOn: false, color: 'yellow' });
但這樣一來,每次修改都得自己合併狀態:
setLight(prev => ({ ...prev, isOn: !prev.isOn }));
雖然這在某些情境下是有用的,但對新手來說比較複雜。
代碼拆解說明
🔍 原始程式碼是這樣:
setLight(prev => ({ ...prev, isOn: !prev.isOn }));
🧠 這是什麼場景?
假設你有一個 state 是物件:
const [light, setLight] = useState({
isOn: false,
color: 'yellow',
});
你想要只改變 isOn
的值(例如把 false 變 true),但保留其他欄位(像 color
)不變。
✅ 拆解這一行的意思
🔹 setLight(...)
這是在更新 light
這個狀態。
🔹 (prev => ...)
這是函式的語法,意思是你要根據「上一個 state 的值」來產生新的值。
這樣寫的好處是:即使 React 還沒更新完畫面,你也能拿到最新的值來做修改。
prev
就是前一個狀態的值,也就是:
{
isOn: false,
color: 'yellow'
}
🔹 { ...prev }
這是「展開運算子(spread operator)」的用法。
意思是:把原本的 prev
裡面的資料都複製過來。像這樣:
{
isOn: false,
color: 'yellow'
}
🔹 isOn: !prev.isOn
這行是覆蓋原本的 isOn
值。
!prev.isOn
的意思是「把原本的布林值反過來」- 如果原本是
false
,就變成true
- 如果原本是
true
,就變成false
📌 小技巧補充:布林值取反
像開關這種布林值切換,可以這樣寫:
setIsOn(prev => !prev);
這樣可以確保你拿到的是「最新狀態」,即使有多次連續的操作,也不會出錯。
📦 結果會變成:
{
isOn: true, // ← 改變了
color: 'yellow' // ← 保持不變
}
然後 React 會用這個新物件來更新畫面。
✅ 等同於這種寫法,但更安全:
你也可以這樣寫,只是比較麻煩:
setLight({
isOn: !light.isOn,
color: light.color,
});
但這樣有個風險:light
這個變數可能不是最新的資料(因為 React 是非同步更新的)。
所以建議用 函式寫法 setX(prev => ...)
更安全。
🧠 小總結
這行程式碼:
setLight(prev => ({ ...prev, isOn: !prev.isOn }));
意思是:
「拿到上一個狀態,把所有欄位複製下來,但把
isOn
的值反過來,然後更新整個狀態。」
React 的哲學是:
「一筆資料,就用一個 state 來管」,這樣程式碼更清楚、修改也更方便。
小結
觀念 | 正確做法 |
---|---|
宣告一個狀態值 | const [x, setX] = useState(initialValue) |
更新狀態,讓畫面跟著改變 | setX(newValue) |
多個資料狀態怎麼寫 | 宣告多個 useState() |
不要這樣寫 | x = newValue(不會更新畫面) |
實作練習
點擊計數器
來練習一個簡單的小功能:每按一下按鈕,數字就加 1。
function ClickCounter() {
const [count, setCount] = useState(0);
return (
<div>
<h2>點擊了 {count} 次</h2>
<button onClick={() => setCount(count + 1)}>再點一下</button>
</div>
);
}
👉 試著自己加上「重置按鈕」看看!
開關燈泡
再來玩個互動的範例:點一下開關,改變燈泡狀態。
function LightBulb() {
const [isOn, setIsOn] = useState(false);
return (
<div>
<p>💡 燈泡目前:{isOn ? '💡 開著' : '🌑 關著'}</p>
<button onClick={() => setIsOn(!isOn)}>
{isOn ? '關燈' : '開燈'}
</button>
</div>
);
}
這樣就可以讓你的元件記得它的狀態,並隨著點擊而變化畫面。
額外技巧:依賴前一個狀態
如果你需要根據「前一個狀態」來更新資料,建議使用函式形式:
setCount(prev => prev + 1);
這樣能確保你拿到的是最新值,避免非同步更新的 bug。
常見錯誤與陷阱
多次 setState 不會立即反映結果
setCount(count + 1);
setCount(count + 1); // 這兩行其實不會 +2,因為 count 值還沒更新
正確方式:
setCount(prev => prev + 1);
setCount(prev => prev + 1); // 這樣才會變成 +2
state 更新後不會立即反映在 console.log
setCount(5);
console.log(count); // ❌ 還是舊值
為畫面還沒更新完,想看到新值請用 useEffect
等待 render。
結論
恭喜你學會 useState
!
接下來,你會發現「當資料改變時要觸發一些動作」是很常見的需求,這時就需要用到另一個 Hook —— useEffect
,它能幫你處理像是:
- 載入 API 資料
- 啟動倒數計時器
- 清理資源
下一篇我們就要開始學習這個超重要的 Hook!