532 字
3 分钟
useImmer

它从哪来#

Immer 是第三方库。React 官方只在 useReducer 参考页里把它当作简化不可变更新的可选方案提了一句,给的例子用的是 useImmerReducer。

React 侧的两个 Hook 来自 use-immer 包,它自身依赖 immer:

Terminal window
npm install use-immer
import { useImmer } from 'use-immer'

不要写成 import { useImmer } from 'immer'。immer 包里没有这个 Hook,这是抄配置时最常见的一处错。

它解决的啰嗦#

用 useState 改一个嵌套三层的字段:

setUser(prev => ({
...prev,
info: {
...prev.info,
basic: { ...prev.info.basic, city: '海淀区' },
},
}))

同样的改动用 useImmer:

setUser(draft => {
draft.info.basic.city = '海淀区'
})

传进回调的是草稿,写法像直接改对象;Immer 在背后算出一份新的不可变状态。

基本用法#

import { useImmer } from 'use-immer'
function App() {
const [user, setUser] = useImmer({
name: '张三',
hobbies: ['读书'],
})
function update() {
setUser(draft => {
draft.name = '李四'
draft.hobbies.push('编程') // 直接 push,不用 [...hobbies, '编程']
})
}
return (
<div>
<p>{user.name}</p>
<p>{user.hobbies.join('、')}</p>
<button onClick={update}>更新</button>
</div>
)
}

签名与 useState 一致:返回 [state, updateState],updateState 同样接受函数形式。

列表的增删改#

setTodos(draft => {
const todo = draft.find(t => t.id === id)
if (todo) todo.completed = !todo.completed
})
setTodos(draft => {
const index = draft.findIndex(t => t.id === id)
if (index !== -1) draft.splice(index, 1)
})
setTodos(draft => {
draft.push({ id: Date.now(), text, completed: false })
})

push、splice、sort 这些可变方法在草稿上都是安全的。

四条边界#

草稿只在回调执行期间有效。别把它存到外部变量,也别留给异步逻辑用。

回调里不要 return 值。这个箭头函数写法是错的:

// 错:返回了赋值结果,Immer 会把它当成新状态
setForm(draft => (draft.theme = 'dark'))
// 对:写成块体
setForm(draft => {
draft.theme = 'dark'
})

原状态仍然不能直接改。user.name = '李四' 一样不触发重渲染,必须通过 setUser。

只有一层、字段不多的状态不值得用它。{ ...prev, age: 21 } 已经够短,为此多引一个依赖不划算。嵌套三层以上,或者要在 reducer 里反复改数组,收益才明显。

配合 reducer#

需要 reducer 时用同包的 useImmerReducer,官方给的就是这个,用法与 useReducer 相同,只是 draft 可以直接改。

第 22 篇,共 39 篇本文属于系列

useImmer
https://www.choria.top/posts/hook-use-immer/
作者
乔瑞雅·克林威尔
发布于
2026-05-02
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

最后更新于 ,距今已过 190 天

部分内容可能已过时