為什麼更新 React 中的 state 要用 immutable 的寫法? 什麼是 immutable? 該如何寫才會是 immutable?
2023年2月1日
在 React 當中,更新 state 是我們很常要做的一件事。然而你知道更新 state 時,應該要用 immutable 的寫法嗎? 為什麼該怎麼做? 怎麼寫才算是 immutable? 上面這一連串的問題,是 React 面試中的高頻題,因為這也是 React 開發者在日常開發中,幾乎天天會遇到的問題。讓我們透過這篇,一起嘗試回答這個面試題目吧。
(編按:因為 immutable 翻成中文很不順,加上在 React 社群大家都直接說 immutable,這邊就不翻成中文了,還請見諒 😅 )
什麼是 immutable?
immutable 是指不可變的,相反地 mutable 則是可變的。在程式語言中的物件被創造後,如果改變了其屬性,我們會說是 mutable。如果是只讀取不改變,則會說是 immutable。
在 React 的脈絡下,如果我們要改變一個 state,我們會用 immutable 的方式,意思是我們不會直接改變該狀態,例如有一個座標位置的 state, 我們要改變該 state,不會用 mutable 的方式直接去改
const [pointerPosition, setPointerPosition] = useState({ x: 0, y: 0 });
// 在 React,我們不會這樣做
pointerPosition.x = 5;
反之,我們會透過 setState
用 immutable 的方式去改。例如這樣
onPointerMove={e => {
setPointerPosition({
x: e.clientX,
y: e.clientY
});
}}
為什麼在 React 要 immutable?
之所以在 React 不能直接去改物件,而是要透過 setState
,是因為物件是傳址 (pass by reference),因此當我們改變了物件本身,該物件在記憶體的位置沒有改變;而當我們改變物件本身,但其位置沒改變時,React 會不知道該物件改變,因此不會用新的值來重新渲染畫面,這將導致畫面渲染出的東西,不是我們預期的結果。
追問題:該怎麼在 React 做到 immutable?
追問: 我們在 React 中要改變 state,都需要傳入一個新的值到 setState
當中,這聽起來沒什麼;在上面的例子,setPointerPosition
裡面,我們傳入新的滑鼠游標位置,也看似沒什麼。不過如果我們想要保留目前物件中的某些值,這該怎麼做到? 舉例來說,有一個登入表單,當使用者輸入完帳號,要輸入密碼時,我們要保留使用者先前輸入的帳號,這樣如何透過 immutable 的方式達成?
const [loginInfo, setLoginInfo] = useState({
account: "",
password: "",
});
如果要保留原物件的某些值,但又要創造一個全新的物件,在 JavaScript 可以透過展開運算子 (speard operator) 來做到,展開運算子就是我們常見的 ...
。以上面這題來說,可以這麼做:
setLoginInfo({
...loginInfo, // 透過展開運算子,複製舊物件的資訊
password: e.target.value, // 把想要覆蓋掉的值,寫在最後面。
});
而在實務上,有時候會容易忘記用這種語法來寫。在社群中,有一些輔助工具也能幫助我們做到輕易寫出 immutable 的方式。舉例來說,React 官方文件與 Redux 都有使用的 Immer 便是社群中很多人會用的工具 (編按:推薦在面試中可以特別提,讓面試官知道你懂 Immer 這類的工具)。
追問題:JavaScript 的陣列方法中,哪些是 immutable?
追問: 在 JavaScript 當中陣列 (array) 也是一種物件,而在 React 若有 state 是陣列,要更新時,一樣需要用 immutable 的方式。請問有哪些陣列方法,是 immutable 的呢?
加入元素到陣列
在陣列中,如果我們要加入新的元素,同時要創造新的陣列,可以用展開運算子 (spread operator),就是常見的 ...
。除此之外,也可以用 concat
。當說到在 JavaScript 加入元素到一個陣列中,很多人會直覺地想到 push
跟 unshift
,push
是加入元素到陣列最後,unshift
則是加入元素到最前面。不過這兩個方法都會直接改變陣列,所以要在 React 更新陣列形式的 state 時,要避免用這兩個。
上面的兩個方法,會是在陣列的最前面或最後面加入元素,但假如要在陣列中插入某個元素,則可以透過 slice
來做到 immutable。透過 slice
先擷取該 index 以前的部分,插入新的元素,再透過 slice
擷取該 index 以後的部分。因為 slice
會回傳新的陣列,因此會是 immutable。
const insertAt = 3; // 想要插入的 index,3 僅為舉例
const newArray = [
...array.slice(0, insertAt),
{ newItem },
...array.slice(insertAt),
];
從陣列中移除元素
在陣列中,如果我們要加入新的元素,同時要創造新的陣列,可以用 filter
這個方法。filter
會移除不符合條件的元素,然後回傳一個新的陣列。在 JavaScript 中,提到移除元素很常會讓人想到 pop
與 shift
, pop
是移除最後的元素,shift
是移除第一個元素。不過這兩個方法都會直接改變陣列,所以要在 React 更新陣列形式的 state 時,要避免用這兩個。
改變陣列中的值
當我們要操作一個陣列時,如果想要改變裡面的值,可以很簡單地透過
array[index] = "new value";
直接改變某個 index 的值。只是在 React 中我們不能這麼做,因為這樣動到原本的陣列,會是 mutable。如果要 immutable 的方式,可以用 map
這個方法,透過以下方式做到不更動原本的陣列,而是複製出一個新的陣列,再改掉我們想要更動的 index 值
const modifiedArray = array.map((item, idx) => {
if (idx === index) {
// 更動我們要的 index 的值
return ...
} else {
// 其他的則不動,直接回傳
return item;
}
});
以上這些方法,可以確保我們在操作陣列時,可以維持 immutable。