为什么更新 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。