Immer.js 在 Redux 生态中为何如此重要
Immer 成为 Redux 生态「标配级」依赖的核心原因,本质是:Immer 完美解决了 Redux 的核心痛点,而且做到了「无痛兼容、极低学习成本、无侵入性」,是 Redux 生态里「降本增效」的最优解。
一、先明确 Redux 的「核心铁律」与「原生痛点」
想要理解 Immer 的价值,必须先记住 Redux 的一个不可违背的核心规则:
Redux 要求:State 是只读的(Read-only)、不可变的(Immutable),永远不能直接修改 state 对象/数组,必须返回一个「全新的状态对象」。
为什么 Redux 要强制「不可变更新」?
这是 Redux 底层的运行根基,原因有2个:
- Redux 的数据变更追踪、组件重新渲染、redux-devtools 时间旅行调试、状态回溯等核心功能,全都依赖「浅比较(shallow equality)」 —— 浅比较只对比引用地址,不对比对象内部的值;
- 如果直接修改原 state(比如
state.name = 'new'),原对象的引用地址不变,Redux 就无法感知到状态发生了变化,会导致:组件不重新渲染、调试工具无法记录变更、状态无法回滚,整个 Redux 体系失效。
Redux 原生「不可变更新」的痛点(噩梦级体验)
Redux 要求返回新对象,但原生 JS 实现「不可变更新」的写法极其繁琐、冗余、易错,尤其是面对「嵌套层级较深」的复杂 state 时,堪称灾难。
原生写法的痛点示例(真实开发场景)
假设 Redux 的 state 是多层嵌套结构:
const state = {
user: {
info: { name: "张三", age: 20 },
address: { province: "北京", city: "朝阳" }
},
todos: [{ id: 1, done: false }, { id: 2, done: true }]
}
需求:把 todos[0].done 修改为 true,同时修改 user.info.name 为「李四」
👉 原生 Redux 必须这么写(手动深拷贝+返回新对象):
// Redux reducer 原生不可变更新写法
return {
...state, // 浅拷贝第一层
user: {
...state.user, // 浅拷贝第二层 user
info: {
...state.user.info, // 浅拷贝第三层 info
name: "李四" // 修改目标属性
}
},
todos: state.todos.map((item, idx) => {
if (idx === 0) return { ...item, done: true } // 数组项也要返回新对象
return item
})
}
这种写法的问题肉眼可见:
- 层级越深,「扩展运算符嵌套」越多,代码量爆炸,可读性极差;
- 手动拷贝容易遗漏层级,一不小心就写出「直接修改原state」的错误代码;
- 数组操作(增删改)需要用
map/filter/concat等返回新数组的方法,不能用push/splice等修改原数组的方法,心智负担极大。
二、Immer.js 是什么?核心原理一句话讲透
Immer 的核心定位
Immer 是一个极小的 JS 库(体积≈2KB),核心只暴露一个「生产函数」:produce,它是 Redux 官方强烈推荐的不可变更新工具,也是 Redux Toolkit(RTK) 的内置核心依赖。
Immer 的核心原理
Immer = 可变的写法 + 不可变的结果 英文总结:Write mutable code, get immutable data
Immer 原理的完整拆解
Immer 内部做了3件核心事,没有任何复杂逻辑,完全是「语法糖级别的优化」:
- 你调用
produce(原状态, 回调函数)时,Immer 会基于「原状态」创建一个临时的代理对象(draft),这个 draft 是原状态的「镜像副本」; - 你在回调函数里,可以像写普通 JS 一样「直接修改」这个 draft 对象 —— 比如
draft.user.info.name = '李四'、draft.todos[0].done = true,所有修改都是「可变写法」,没有任何嵌套和拷贝; - Immer 内部会自动追踪你对 draft 的所有修改,当回调执行完毕后,Immer 会基于你的修改,返回一个「全新的、不可变的状态对象」,原状态完全不会被修改,完美符合 Redux 的不可变要求。
核心关键点:你修改的是「draft副本」,不是原state;最终返回的新对象,引用地址一定和原state不同,Redux 能完美感知到状态变更。
三、Immer 如何解决 Redux 痛点?
还是上面那个需求:修改 todos[0].done = true + 修改 user.info.name = '李四',用 Immer 实现 Redux reducer:
import { produce } from "immer"
// Immer 版本的 reducer
const reducer = (state, action) => {
return produce(state, (draft) => {
// ✅ 完全是「可变写法」,想怎么改就怎么改,无嵌套、无拷贝
draft.user.info.name = "李四"
draft.todos[0].done = true
})
}
对比原生写法,Immer 带来的核心收益
- 彻底简化代码:多层嵌套的复杂更新,一行代码搞定,代码量减少 80% 以上,可读性拉满;
- 消除心智负担:不用再记「扩展运算符、map/filter、concat」等不可变技巧,不用再担心「漏拷贝层级」;
- 极低学习成本:只要会写普通 JS 的「可变修改」,就会用 Immer,0 学习成本;
- 零错误率:再也不会写出「直接修改原state」的错误代码,彻底规避 Redux 最常见的 bug;
- 完全兼容:返回的结果和原生写法的「不可变新对象」完全一致,Redux 感知不到任何差异,无缝集成。
四、Immer 在 Redux 生态中的「核心地位」
Immer 不是「可选插件」,而是 Redux 生态从 v4 开始的事实标准,核心原因有 3 个,优先级从高到低:
1. Redux 官方「最高级别的认可」:深度绑定,内置集成
-
Redux 核心团队(Dan Abramov 等人)在 Redux 文档中,将 Immer 列为「不可变更新的首选方案」,明确推荐所有开发者使用;
-
Redux Toolkit (RTK) —— Redux 官方推出的「现代 Redux 最佳实践封装」,是目前 Redux 开发的唯一推荐方式,而 RTK 内部直接内置了 Immer,所有的
createSlice/createReducer都默认基于 Immer 实现,你甚至不需要手动导入produce,直接写「可变代码」即可。举个 RTK 的真实开发示例(日常开发的标准写法):
javascriptimport { createSlice } from '@reduxjs/toolkit' const userSlice = createSlice({ name: 'user', initialState: { /* 初始嵌套状态 */ }, reducers: { updateUserInfo: (state, action) => { // ✅ RTK 内置 Immer,直接修改 state(实际是 draft)即可,无需 produce state.user.info.name = action.payload.name state.todos[0].done = true } } })这是 Redux 官方钦定的「最优写法」,而这一切的核心就是 Immer。
2. 完美契合 Redux 生态的「核心诉求」:无侵入、无副作用、轻量
Redux 生态的核心诉求是「简洁、可维护、无黑魔法」,而 Immer 完全满足:
- 无侵入:Immer 只在 reducer 内部做处理,不修改 Redux 的任何核心逻辑,不影响中间件、devtools 等生态工具;
- 无副作用:原 state 永远不会被修改,返回的新 state 完全符合不可变规范,不会引入任何潜在 bug;
- 极致轻量:体积仅 2KB 左右,引入后对项目打包体积几乎无影响;
- 性能优秀:Immer 的代理追踪机制是「惰性的」,只追踪你修改的属性,性能和手动不可变更新几乎持平,甚至在复杂嵌套场景下更快。
3. 解决了 Redux 最大的「入门门槛」和「团队协作痛点」
Redux 被很多开发者吐槽「难学、难用」,核心原因就是「不可变更新」的写法太反人类。
- 对新手:Immer 让新手可以快速上手 Redux,不用先死记硬背「不可变更新」的各种技巧,降低了 Redux 的学习曲线;
- 对团队:统一了状态更新的写法,避免了团队中有人写原生嵌套拷贝、有人写 Lodash 的
cloneDeep、有人直接修改原 state 的混乱情况,提升了团队协作效率。
五、补充:Immer 的两个核心使用原则
Immer 虽然好用,但有两个「极简原则」必须遵守,否则会写出错误代码,这也是 Redux 官方强调的点:
原则 1:在 Immer 的回调函数中,要么「修改 draft」,要么「return 新值」,二选一,不要混用
正确写法1(推荐):修改 draft,不 return 任何值
produce(state, draft => {
draft.name = '李四' // ✅ 正确,只修改 draft
})
正确写法2:不修改 draft,直接 return 新值
produce(state, draft => {
return { ...draft, name: '李四' } // ✅ 正确,return 新值
})
错误写法:既修改 draft,又 return 新值(Immer 会忽略你的 draft 修改,只认 return 的值)
produce(state, draft => {
draft.name = '李四'
return { age: 20 } // ❌ 错误,最终只会返回 { age:20 },draft 修改无效
})
原则 2:不要在回调函数中「手动 return draft」
draft 是 Immer 的「临时代理对象」,不是最终的不可变对象,手动 return draft 会破坏不可变性,导致 Redux 无法感知状态变化:
produce(state, draft => {
draft.name = '李四'
return draft // ❌ 错误!draft 是代理对象,不是最终的新状态
})
正确做法:修改 draft 后,什么都不要 return,Immer 会自动帮你生成并返回正确的新状态。
总结(核心知识点提炼,必记)
- Redux 的核心铁律是 State 不可变,原生实现不可变更新的痛点是「嵌套深、代码繁、易出错」;
- Immer 的核心价值是:允许你用「可变的写法」,生成「不可变的结果」,完美适配 Redux 的规则;
- Immer 的原理极简:创建 draft 代理对象 → 追踪修改 → 生成全新不可变状态,无黑魔法;
- Immer 在 Redux 生态中「不可或缺」的核心原因:
- ✅ 官方强推+RTK内置,是现代 Redux 的标配;
- ✅ 彻底解决 Redux 最大的写法痛点,降本增效;
- ✅ 无侵入、轻量、兼容所有生态,无任何副作用;
- 一句话概括:没有 Immer,现代 Redux 开发的体验会倒退一大步,Immer 不是 Redux 的「锦上添花」,而是「雪中送炭」。