Immer 是一个 JavaScript 库,用于简化处理不可变数据的操作。它的目标是使数据的修改过程更加直观和易于理解,同时保持不可变性的特性。传统上,处理不可变数据需要进行大量的手动操作,例如深层复制对象、创建新的数组副本等。这样的操作不仅繁琐,而且容易出错。Immer 的出现解决了这个问题。
React 的设计理念鼓励使用不可变数据来提高性能和可维护性,因为它可以帮助我们更好地追踪数据的变化,进行状态比较,并准确地确定需要重新渲染的组件。在 React 项目中,如果要更改的组件不是仅有一个基本类型的属性值,则需要深层复制状态对象,然后与新属性值进行合并,不能直接更改原有状态。
问题
当数据层级不深时没有太大的问题,但是当我们遇到一个 state 很复杂的对象或者数组问题就显现出来了,比如我们要更新 lat 字段:
const [state, setState] = useState({
id: 1,
title: '豪方花园'
city: '深圳市',
address: {
street: '南山区南海大道 4040 号',
zipcode: '518000',
geo: {
lat: 22.548669,
lng: 113.941641
}
}
})
setState 的写法就会是这样:
setState({
...state,
address: {
...state.address,
geo: {
...state.address.geo,
lat: 21.125
}
}
})
这种写法就非常的啰嗦了,只需要更新一个字段,却需要三次展开复制合并数据,一不留神就出 bug 了。在无法修改数据结构的情况下咋解决这个问题呢?我知道的最好办法就是使用 Immer。
Immer
Immer 是一个用于处理不可变数据的 JavaScript 库,它通过引入一个简单的可变草稿(draft)概念,使得在不直接修改原始数据的情况下进行更新变得更加容易和直观。基本原理如下:
Immer 原理(来源官方文档)
- 创建草稿(Draft):使用 Immer 的
produce
函数,你可以传入一个原始数据对象以及一个函数,该函数描述了如何修改数据。Immer 将根据这个函数创建一个草稿副本。 - 在草稿上进行修改:在草稿中,你可以像在常规 JavaScript 对象上一样直接修改数据,而不用担心不可变性。Immer 会记录每个修改操作。
- 生成新的不可变状态:当你完成对草稿的修改时,你可以从草稿中生成一个新的不可变状态。Immer 会比较原始数据和草稿之间的差异,并生成一个全新的不可变状态对象。
- 避免不必要的修改:Immer 通过结构共享和惰性复制等技术,确保只有真正发生变化的数据才会被复制。这样可以减少不必要的内存开销。
使用 Immer 非常简单,首先项目中安装 Immer:
npm install -D immer
以下是使用 Immer 的基本示例:
import produce from 'immer'
const state = {
count: 0,
todos: []
}
const newState = produce((draft) => {
draft.count += 1
draft.todos.push('New todo')
})
在上面的例子中,我们使用 produce
函数创建了一个新的状态 newState
。在草稿函数中,我们可以直接对 draft
进行修改,而不需要担心不可变性。Immer 会自动处理这些修改并生成一个新的不可变状态。
接下来,我们使用 Immer,将前面更新经度的案例修改一下:
import { useState } from 'react'
import { produce } from 'immer'
const Map = () => {
const [state, setState] = useState({
id: 1,
title: '豪方花园',
city: '深圳市',
address: {
street: '南山区南海大道 4040 号',
zipcode: '518000',
geo: {
lat: 22.548669,
lng: 113.941641
}
}
})
function handleUpdate() {
setState(
produce((draft) => {
draft.address.geo.lat = 21.125
})
)
}
return (
<>
<div>{state.address.geo.lat}</div>
<button onClick={handleUpdate}>Update</button>
</>
)
}
export default Map
由于所有 state 的更新都使用 produce
包装的更新模式,所以我们可以通过将更新模式包装在 use-immer
包中来简化上述操作
useImmer Hook
useImmer
是一个自定义 React Hook,它基于 Immer 库,用于在函数组件中管理可变状态。useImmer
简化了使用 Immer 的操作过程,并提供了一种方便的方式来更新状态并触发组件重新渲染。
useImmer
的基本原理如下:
- 在组件内部使用
useImmer
Hook:在函数组件中使用useImmer
Hook,类似于使用其他 React Hook。它接受一个初始状态(可以是一个值或一个函数),并返回一个包含状态值和状态更新函数的数组。 - 创建状态和更新函数:在
useImmer
内部,它会使用useState
来创建一个状态值(称为state
)和一个更新函数(称为setState
)。初始状态会被传递给useState
,并返回一个包含状态和更新函数的数组。 - 使用 Immer 创建草稿:
useImmer
在内部使用 Immer 的produce
函数创建一个草稿副本。这个草稿副本会与初始状态相对应,并用于记录状态的修改。 - 状态更新函数:
setState
函数被返回给组件,供组件在需要更新状态时调用。当调用setState
时,它会接收一个更新函数作为参数,该函数描述了如何修改状态。在更新函数内部,你可以直接修改草稿,就像在常规 JavaScript 对象上一样。 - 生成新的状态和触发重新渲染:当在更新函数中完成对草稿的修改时,Immer 会根据草稿和初始状态之间的差异生成一个全新的不可变状态。然后,
useImmer
将更新组件的内部状态,并触发组件的重新渲染。
这样,每当状态更新函数被调用时,useImmer
会处理 Immer 相关的逻辑,使得在组件中处理可变状态变得更加简洁和直观。
以下是一个使用 useImmer
的简单示例:
import { useImmer } from 'use-immer'
function Counter() {
const [state, setState] = useImmer({ count: 0 })
const increment = () => {
setState((draft) => {
draft.count += 1
})
}
// ...
}
在上面的例子中,我们使用 useImmer
创建了一个可变状态 state
和一个状态更新函数 setState
。在 increment
函数中,我们使用 setState
来更新状态。在更新函数内部,我们可以直接修改草稿对象 draft
的属性,而无需担心不可变性。useImmer
会自动处理状态的更新和重新渲染。
正如你看到的,useImmer
就是对 useState
Hook 和 produce
函数的封装,在 useImmer
中,你甚至看不到 produce
函数的影子。
接下来,我们使用 Immer,将前面更新经度的案例重写一下:
import { useImmer } from 'use-immer'
import { useImmer } from 'use-immer'
const Map = () => {
const [state, setState] = useImmer({
id: 1,
title: '豪方花园',
city: '深圳市',
address: {
street: '南山区南海大道 4040 号',
zipcode: '518000',
geo: {
lat: 22.548669,
lng: 113.941641
}
}
})
function handleUpdate() {
setState((draft) => {
draft.address.geo.lat = 21.125
})
}
// ...
}
// ...
数组操作
React 中对数组增删改操作如果不使用 Immer,对新手来说不是那么好理解:
function handleAddTodo(title) {
setTodos([
...todos,
{
id: nextId++,
title: title,
done: false
}
])
}
function handleChangeTodo(nextTodo) {
setTodos(
todos.map((t) =>
t.id === nextTodo.id
? {
...t,
title: nextTodo.title,
done: nextTodo.done
}
: t
)
)
}
function handleDeleteTodo(todoId) {
setTodos(todos.filter((t) => t.id !== todoId))
}
使用 Immer 或 userImmer
代码就清晰多了:
function handleAddTodo(title) {
setTodos((draft) => {
draft.push({
id: nextId++,
title: title,
done: false
});
});
}
function handleChangeTodo(nextTodo) {
setTodos((draft) => {
const todo = draft.find((t) => t.id === nextTodo.id) as Todo;
todo.title = nextTodo.title;
todo.done = nextTodo.done;
});
}
function handleDeleteTodo(todoId) {
setTodos((draft) => {
const index = draft.findIndex((t) => t.id === todoId);
draft.splice(index, 1);
});
}
毕竟更改可变数据比更改不可变数据好理解很多,操作复杂数据也方便很多。Redux Toolkit 内置了 Immer,可见其重要性。如果你使用 Vue.js,就没必要使用 Immer 了,因为 Vue.js 已经实现了可变数据的响应式状态。
总结
使用 Immer 可以简化 React 应用中的状态管理,使得对不可变数据的更新更加直观和容易,同时减少不必要的内存开销。当然,Immer 不局限于只能在 React 应用中使用,需要简化不可变数据操作的地方都可以使用它。
评论 (0)