Jeff的隨手筆記

學習當一個前端工程師

0%

『Day 16』理解 React 的陣列狀態更新

前言

在前幾篇文章中,我們討論了 React 的狀態管理機制,包括狀態的快照特性以及批次更新的概念。今天想要分享一個在處理陣列型態狀態時常被忽略,但卻非常重要的觀念:不可變更新(Immutable Update)。這個觀念讓我在剛開始學習 React 時感到困惑,但理解後對於寫出更穩定的 React 應用程式有很大的幫助。

React 的淺比較機制

在深入討論之前,我們需要先了解 React 是如何判斷狀態是否有變化。React 使用了一個叫做「淺比較」的機制,它的運作方式其實很直觀:

  • 對於基本型別(數字、字串、布林),React 會直接比較它們的值
  • 對於物件和陣列,React 只會比較它們的參考位址,而不會深入比較內容

讓我們用一個簡單的例子來理解:

1
2
3
4
5
6
7
const arr1 = ['蘋果', '香蕉'];     // 新的記憶體位置
const arr2 = ['蘋果', '香蕉']; // 另一個新的記憶體位置
const arr3 = arr1; // 指向 arr1 的記憶體位置

console.log(arr1 === arr2); // false,不同記憶體位置
console.log(arr1 === arr3); // true,相同記憶體位置

這個特性告訴我們一個重要的事實:當我們在 React 中直接修改陣列時(例如使用 push 方法),雖然內容改變了,但陣列本身的參考位址維持不變。

而 React 在判斷狀態是否更新時,是透過比較參考位址來判斷的。

就像是我們修改了筆記本的內容,但對 React 來說,它只會確認「是不是同一本筆記本」,而不會去檢查筆記本裡的內容是否改變了。

這就解釋了為什麼直接修改陣列內容時,即使我們用 setState 更新狀態,React 可能還是會認為「這是同一個陣列,沒有變化」,進而不觸發重新渲染。這也是為什麼我們需要建立新的陣列來更新狀態,而不是直接修改原有的陣列。

常見的錯誤與正確做法

在實務開發中,我曾經看過(也寫過!)這樣的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ❌ 錯誤示範:直接修改原陣列
function FruitList() {
const [fruits, setFruits] = useState(['蘋果', '香蕉']);

const addFruit = () => {
fruits.push('橘子'); // 參考位址沒變
setFruits(fruits); // React 可能認為狀態沒改變!
}
// ...
}

// ✅ 正確做法:建立新的陣列
function FruitList() {
const [fruits, setFruits] = useState(['蘋果', '香蕉']);

const addFruit = () => {
setFruits([...fruits, '橘子']); // 創建新的參考
}
// ...
}

在第一個例子中,雖然我們確實修改了陣列的內容,但因為陣列的參考位址沒有改變,React 可能無法察覺到這個變化。而第二個例子中,我們透過展開運算符創建了一個全新的陣列,這樣 React 就能正確偵測到變化了。

處理巢狀結構

在實際開發中,我們常常會遇到更複雜的資料結構,像是陣列中包含物件的情況:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: '學習 React', done: false },
{ id: 2, text: '寫部落格', done: false }
]);

const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, done: !todo.done } // 建立新的物件
: todo
)); // 建立新的陣列
};
// ...
}

這種方式的好處

採用不可變更新的方式來處理陣列狀態,帶來了幾個重要的優勢:

  1. 可預測性:避免直接修改原陣列可能帶來的意外副作用
  2. 效能優化:React 能夠正確地追蹤變化,只更新真正需要更新的部分
  3. 除錯方便:每次更新都會產生新的參考,讓我們更容易追蹤資料的變化

總結

在 React 中處理陣列狀態時,記得要遵循不可變更新的原則。雖然這樣寫可能會多打一些程式碼,但這些「額外」的工作其實是在幫助我們建立更穩定、可維護的應用程式。理解並善用這個概念,能讓我們在處理複雜的狀態更新時更加得心應手。