为什么更新 React 中的 state 要用 immutable 的写法? 什么是 immutable? 该如何写才会是 immutable?

2023年2月1日

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

在 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 上追蹤我們