Jeff的隨手筆記

學習當一個前端工程師

0%

『Day 18』 React State 的設計原則

前言

在 React 的開發過程中,好的狀態管理不只關乎程式碼是否容易理解,更直接影響到專案的可維護性與穩定性。還記得我在學習 React 時,當課程進度進入到 state 的部分,一開始覺得還蠻簡單的,直到後來要重構整個程式碼時才發現:原來不好的 state 設計會讓程式變得多麽難維護。那時候真的是被 state 管理搞得焦頭爛額。

今天我們就來看看 React 官方對於狀態設計的建議。相信這些原則不只能幫助我們寫出更好的程式碼,也能讓其他正在學習的夥伴少走一些彎路。

相關狀態要懂得「抱團」

在開發的初期,我常常會直覺地為每個需要追蹤的數值建立一個獨立的 state。但到最後才發現,當多個狀態總是一起更新時,分開管理可能導致程式碼難以維護。將相關狀態整合在一起,能使程式碼更簡潔、邏輯更清晰。

假設我們要追蹤滑鼠在畫面上的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function MouseTracker() {
// 獨立的 state
const [x, setX] = useState(0);
const [y, setY] = useState(0);

return (
<div onMouseMove={(e) => {
setX(e.clientX);
setY(e.clientY);
}}>
滑鼠位置:{x}, {y}
</div>
);
}

這樣寫雖然可以運作,但當我們把相關的狀態組織在一起時,程式碼會更容易理解和維護:

1
2
3
4
5
6
7
8
9
10
11
12
function MouseTracker() {
// ✅ 優化後的寫法:將相關的數據組織在一起
const [position, setPosition] = useState({ x: 0, y: 0 });

return (
<div onMouseMove={(e) => {
setPosition({ x: e.clientX, y: e.clientY });
}}>
滑鼠位置:{position.x}, {position.y}
</div>
);
}

總結來說,將相關的狀態整合在一起不僅能減少重複更新的麻煩,也能讓程式碼的邏輯更直觀。

更新物件型態 State

React 的 setState 預設是完全取代舊的狀態,而非進行部分合併。這樣的設計是為了避免預設合併邏輯帶來的不確定性,因此需要我們手動操作。

因此當我們使用物件作為 state 時,更新的方式需要特別注意,不然很容易發生資料不見的問題。

讓我用一個實際的例子來說明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function UserProfile() {
const [user, setUser] = useState({
name: '小明',
age: 25,
email: 'ming@example.com'
});

// ❌ 這樣寫會出問題!
const updateName = () => {
setUser({ name: '小華' });// 糟糕,age 和 email 不見了!
};

return (
<div>
<p>姓名:{user.name}</p>
<p>年齡:{user.age}</p>
<p>Email:{user.email}</p>
<button onClick={updateName}>更新姓名</button>
</div>
);
}

當我們點擊按鈕時,會發現畫面上只剩下名字,年齡和 email 都不見了!這是因為在 React 中,setState 不會幫我們合併物件,而是直接用新的物件取代舊的。

正確的做法是使用展開運算符(…)來保留其他欄位:

1
2
3
4
5
6
7
// ✅ 正確的寫法
const updateName = () => {
setUser({
...user,// 先保留原本所有的資料
name: '小華'// 再更新想改的部分
});
};

這樣寫有幾個好處:

  1. 透過展開運算符(...)保留原本的資料,避免不小心遺失其他欄位
  2. 程式碼的意圖更明確 - 我們可以清楚看到哪些是要保留的,哪些是要修改的部分
  3. 當物件結構變得更複雜時,這種更新模式依然可以保持程式碼的可維護性

這就是為什麼我們會想把相關的資料放在同一個物件裡面 - 既可以一次完整地保留所有需要的資料,又能清楚地表達程式的意圖。

說真的,我第一次寫 React 時也在這裡踩過地雷。看到資料莫名消失時,整個人都傻了:「蛤?我明明就只改個名字而已,怎麼其他資料都不見了?」這個經驗讓我更深刻理解到,在操作物件型態的 state 時,正確的更新方式能幫助我們避免很多意想不到的問題。

避免狀態矛盾

在處理表單或複雜的使用者互動時,我們經常需要追蹤多個相關的狀態。這時候很容易不小心製造出互相矛盾的狀態。舉個例子,假設我們在處理一個表單提交的流程:

1
2
3
4
5
6
7
8
9
10
11
12
function FeedbackForm() {
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);

async function handleSubmit(e) {
e.preventDefault();
setIsSending(true);
await sendMessage();
setIsSending(false);
setIsSent(true);
}
}

這樣的設計看似合理,但如果網路請求失敗了呢?我們可能會遇到 isSendingisSent 狀態不一致的問題。

更好的做法是使用單一的狀態來表達這個流程:

1
2
3
4
5
6
7
8
9
10
11
function FeedbackForm() {
const [status, setStatus] = useState('idle'); // 'idle' | 'sending' | 'sent'

async function handleSubmit(e) {
e.preventDefault();
setStatus('sending');
await sendMessage();
setStatus('sent');
}
}

避免不必要的 State

在實務上,我常看到開發者把可以從現有資料計算出來的值也放進狀態。這不只增加了程式碼的複雜度,更容易導致資料不同步的問題。

比方說,處理使用者的全名:

1
2
3
4
5
6
7
8
9
10
// 不建議的做法
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState(''); // 這是多餘的!

// 建議的做法
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`; // 直接計算就好

總結

好的狀態設計能讓我們的程式碼更容易維護,也能避免許多常見的錯誤。根據我的經驗,遵循以下原則能幫助我們寫出更好的 React 應用:

  1. 將相關的狀態組織在一起:減少重複更新,讓邏輯更清楚。
  2. 使用單一狀態來表示多個相關狀態:避免矛盾或不一致的狀態。
  3. 避免將可計算的值放入狀態:利用即時計算來減少狀態同步問題。
  4. 保持狀態結構扁平化:簡化狀態更新邏輯。

記住,在設計狀態時要追求簡單但不過度簡化。找到這個平衡點需要經驗的累積,但只要持續實踐這些原則,相信大家都能寫好 React 。