為什麼更新 React 中的 state 要用 immutable 的寫法? 什麼是 immutable? 該如何寫才會是 immutable?

2023年2月1日

💎 加入 E+ 成長計畫 與超過 500+ 位軟體工程師一同在社群中成長,並且獲得更多的軟體工程學習資源

在 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 加入元素到一個陣列中,很多人會直覺地想到 pushunshiftpush 是加入元素到陣列最後,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 中,提到移除元素很常會讓人想到 popshiftpop 是移除最後的元素,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。

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們