被称为“龙虾之父”的 Michel Weststrate 是 Immer 和 MobX 的作者,他在前端社区的影响力毋庸置疑。Immer 自发布以来,几乎成了 React 项目中处理不可变数据更新的标配工具,配合 useImmer 和 useImmerReducer 这两个 Hook,开发者可以用可变的写法实现不可变的数据更新,大幅降低了手写展开运算符的心智负担。然而就是这样一位把 Immer 推向巅峰的人,却公开表示自己在 Hook 中使用 Immer 的方式已经不再是首选方案,这条推文一出,社区里关于“Hook 状态管理是否走错了方向”的讨论瞬间被点燃。

先搞清楚 Immer 到底解决了什么问题
React 的核心机制是基于引用相等性来判断数据是否变化,从而决定组件是否需要重新渲染。这就要求所有状态更新都必须产生新的引用,而不能直接修改原对象。在没有 Immer 的年代,开发者只能通过层层展开运算符来复制嵌套对象,代码既冗长又容易出错。比如要更新一个深层嵌套的列表项属性,往往需要写出三四层展开的代码,稍有疏忽就会意外修改了原始数据。
Immer 的思路是利用 Proxy 拦截所有的写操作,把你对草稿对象的修改记录下来,最后基于这些修改生成一颗新的不可变树。这样你就可以写出看似可变的代码,Immer 会在背后帮你完成结构共享的拷贝。这种方式确实优雅,也正因为如此,useImmer 在 Hook 时代迅速流行开来。
import { useImmer } from 'use-immer';
function TodoList() {
const [todos, updateTodos] = useImmer([]);
const toggleDone = (id) => {
updateTodos((draft) => {
const item = draft.find((t) => t.id === id);
if (item) item.done = !item.done;
});
};
return <ul>{todos.map((t) => <li key={t.id}>{t.text}</li>)}</ul>;
}这段代码看起来非常自然,但在这种自然背后,隐藏着一些容易被忽视的成本。Proxy 拦截是有运行时开销的,对于高频更新的大对象树,这个开销会被放大。同时,草稿对象的行为在某些边界情况下与普通对象并不完全一致,例如在草稿上调用某些原生方法或者将草稿对象传出更新函数,都可能触发难以排查的问题。
Michel Weststrate 那条推文真正想说什么
如果只看推文表面,很容易误读为“Immer 被废弃了”,但实际上作者表达的核心观点是:在 Hook 时代,把状态管理的复杂度全部压在组件内部的 useState 系列方案上,本身就是一个值得反思的架构选择。他多次强调,Immer 作为底层库依然健康,问题在于开发者把它和 Hook 深度绑定后形成的那套使用范式。
具体来说,当一个应用的状态全部通过 useImmer 散落在各个组件里时,状态的归属变得模糊,组件的重渲染范围难以控制,跨组件的状态同步也需要借助 Context,而 Context 的粗粒度更新会让性能问题雪上加霜。作者本人更倾向于把状态收敛到外部 store 中,组件通过订阅的方式来消费状态,这样状态的变更边界清晰,也更容易做性能调优。
React 官方后来提供的 useSyncExternalStore 这个 Hook 其实印证了这一方向。它为外部 store 与 React 的并发渲染之间提供了标准化的桥接方式,保证了在撕裂场景下的一致性。也就是说,React 社区的主流思路正在从“状态放进组件”转向“状态放到外部,组件只做订阅与渲染”。
不可变更新还有必要吗,新的实践建议
即便转向外部 store,不可变性本身依然重要,它是 React 渲染优化的基石。Immer 在纯数据层的使用依然有价值,比如在 Zustand、Redux Toolkit 的 reducer 中,produce 依然是简化深层更新的利器。变化的只是 Immer 的使用位置:从组件内的 Hook 层面,退回到状态库的更新函数层面。
import { create } from 'zustand';
import { produce } from 'immer';
const useStore = create((set) => ({
user: { profile: { name: '', tags: [] } },
updateName: (name) =>
set((state) =>
produce(state, (draft) => {
draft.user.profile.name = name;
})
),
}));
// 组件只负责订阅,粒度可以精确到单个字段
function NameDisplay() {
const name = useStore((s) => s.user.profile.name);
return <span>{name}</span>;
}这种模式把 Proxy 的开销限制在真正发生更新的那一刻,组件订阅的是具体的字段切片,只有被订阅的数据变化时才会重渲染。相比把整个草稿对象塞进 Context,性能表现和可维护性都有明显提升。
对于新项目,可以参考这样几条原则:第一,局部 UI 状态继续用 useState,简单直接;第二,跨组件共享的领域状态交给外部 store,并用 selector 控制订阅粒度;第三,深层不可变更新统一在 store 的更新逻辑里用 produce 处理,不在组件里直接操作草稿;第四,如果一个对象树特别庞大且更新极其频繁,可以评估放弃 Proxy 方案,改用细粒度的原子化状态库,避免为不需要更新的部分付出代理成本。
龙虾之父的一条推文并非宣判某个工具的死刑,而是提醒大家不要把任何一种模式当作永恒的最佳实践。技术选型永远要回到具体场景:状态复杂度、团队熟悉度、性能敏感度,这三者共同决定了哪条路更适合你。理解了 Immer 的原理边界与 Hook 状态管理的演进逻辑,你就能在下一轮范式变化到来时,比社区跑得更快一步。
ImmerReact Hook状态管理修改时间:2026-09-06 03:02:32