Zustand 状态更新指南:浅合并、深层嵌套与 Immer/optics-ts/Ramda 不可变更新方案

发布时间:2026/9/18 1:26:13

Zustand 状态更新指南:浅合并、深层嵌套与 Immer/optics-ts/Ramda 不可变更新方案 Zustand 状态更新指南浅合并、深层嵌套与 Immer/optics-ts/Ramda 不可变更新方案【免费下载链接】zustand Bear necessities for state management in React项目地址: https://gitcode.com/gh_mirrors/zu/zustandZustand 通过set函数完成 store 内状态的更新默认采用浅合并shallow merge语义让扁平对象的状态更新变得极其简单而面对深层嵌套对象时则需要显式地以不可变方式逐层复制。本文以 docs/learn/guides/updating-state.md 为核心结合 src/vanilla.ts、src/middleware/immer.ts 与仓库测试用例系统讲解扁平更新、嵌套对象更新的四种主流方案手写展开、Immer、optics-ts、Ramda并剖析set的浅合并与replace标志的底层实现帮助你写出更简洁、类型安全、可维护的状态更新代码。扁平更新Flat updates直接调用set即可Zustand 更新状态非常简单调用create时传入的set函数传入新的状态或返回新状态的函数新状态会与 store 中已有状态进行浅合并shallow merge。这意味着你不需要手动展开旧状态只需要写出真正变化的字段。下面是官方文档给出的经典示例——一个管理firstName/lastName的 Person storeimport { create } from zustand type State { firstName: string lastName: string } type Action { updateFirstName: (firstName: State[firstName]) void updateLastName: (lastName: State[lastName]) void } // Create your store, which includes both state and (optionally) actions const usePersonStore createState Action()((set) ({ firstName: , lastName: , updateFirstName: (firstName) set(() ({ firstName: firstName })), updateLastName: (lastName) set(() ({ lastName: lastName })), }))在消费组件中用 selector 精准挑选需要的状态与 actionfunction App() { // select the needed state and actions, in this case, the firstName value // and the action updateFirstName const firstName usePersonStore((state) state.firstName) const updateFirstName usePersonStore((state) state.updateFirstName) return ( main label First name input // Update the firstName state onChange{(e) updateFirstName(e.currentTarget.value)} value{firstName} / /label p Hello, strong{firstName}!/strong /p /main ) }几点实操要点selector 驱动重渲染组件通过usePersonStore((state) state.firstName)订阅指定切片Zustand 的 React 绑定在 src/react.ts 中基于React.useSyncExternalStore实现只有被 selector 选中的值发生变化时组件才会重渲染因此不要省略 selector 以免订阅整个 store。两种传参形式set既接受对象字面量set({ firstName })也接受函数set((state) ({ ... }))。函数形式能拿到当前最新状态是基于旧值计算新值场景下的推荐写法。在 src/vanilla.ts 中setState会先判断partial是否为函数若是则以当前state为参数调用它得到最终nextState。Object.is变更检测setState内部用!Object.is(nextState, state)见 src/vanilla.ts判断状态是否真正变化未变化则跳过通知避免无效的订阅回调与重渲染。浅合并的底层原理浅合并的魔法来自 src/vanilla.ts 的setState实现。当未传入replace即replace为undefined且nextState是普通对象时state (replace ?? (typeof nextState ! object || nextState null)) ? (nextState as TState) : Object.assign({}, state, nextState)Object.assign({}, state, nextState)把nextState的字段覆盖到state的副本上只做一层合并这正是 docs/learn/guides/immutable-state-and-merging.md 中强调的因为set默认合并所以常见的计数更新可以直接写set((state) ({ count: state.count 1 }))而无需手写set((state) ({ ...state, count: state.count 1 }))若nextState是null、undefined等非对象则直接整体替换为nextState。深层嵌套对象Deeply nested object必须不可变地逐层更新当状态是如下这类深层嵌套结构时情况就没那么简单了type State { deep: { nested: { obj: { count: number } } } }由于set只做一层浅合并deep字段下层的对象会整段被替换因此更新嵌套状态必须确保过程是**不可变immutable**的——保留未变部分的对象引用只替换变化路径上的对象。常规方案手动展开spread逐层复制与 React / Redux 的常规做法一致用展开运算符...逐层复制状态对象并手动把新值合并进去normalInc: () set((state) ({ deep: { ...state.deep, nested: { ...state.deep.nested, obj: { ...state.deep.nested.obj, count: state.deep.nested.obj.count 1 } } } })),这段代码功能正确但非常冗长——每加深一层就要多写一层...展开。层级越深样板代码越多也越容易在展开时漏掉某个层级导致意外的引用共享或状态丢失。下面介绍几种让生活更轻松的替代方案。使用 Immer用可变写法实现不可变更新很多人使用 Immer 来更新嵌套值。Immer 可以在任何需要更新嵌套状态的场景下使用例如 React、Redux当然也包括 Zustand。利用 Immer 可以大幅缩短深层嵌套对象的更新代码。来看示例immerInc: () set(produce((state: State) { state.deep.nested.obj.count })),更新量减少非常明显Immer 通过 Proxy 提供可变的draft你在回调里直接修改嵌套属性Immer 会记录这些修改并产出一个新的不可变状态。配合 immer middleware 使用如果不想在每个 action 里手动包一层produceZustand 官方提供了 immer 中间件 src/middleware/immer.ts。它的核心实现是重写setStatestore.setState (updater, replace, ...args) { const nextState ( typeof updater function ? produce(updater as any) : updater ) as ((s: T) T) | T | PartialT return set(nextState, replace as any, ...args) }即当 updater 是函数时自动套上produce于是你可以直接写import { create } from zustand import { immer } from zustand/middleware/immer export const useCountStore createState Actions()( immer((set) ({ count: 0, increment: (qty: number) set((state) { state.count qty }), decrement: (qty: number) set((state) { state.count - qty }), })), )注意使用中间件时create后面需要保留多余的括号createT()(...)具体原因见 advanced-typescript.md。需要先安装 Immer 作为直接依赖npm install immer。仓库的 tests/immer.test.tsx 验证了函数 updater 会经produce处理同时 tests/middlewareTypes.test.tsx 验证了其类型推导——在回调中直接state.count get().count 1也能获得完整类型检查。使用 Immer 的注意事项Gotchas请务必阅读官方文档中列出的 immer-middleware.md 中记录的注意事项订阅没有被调用使用 Immer 时务必遵守 Immer 的规则。例如类对象需要添加[immerable] true才能被 Proxy 代理。如果没这么做Immer 仍会直接修改对象而非通过代理从而同时改动当前状态由于 Zustand 会用Object.is检查状态是否真的变化见 src/vanilla.ts前后状态相等时就会跳过订阅回调。replace标志依然生效中间件把replace原样透传给底层settests/immer.test.tsx 验证了setState({ a: 9 }, true)会整体替换状态。使用 optics-ts无 Proxy 的函数式光学另一个选项是使用 optics-tsopticsInc: () set(O.modify(O.opticState().path(deep.nested.obj.count))((c) c 1)),optics-ts 的核心思路是光学optic——用O.opticState().path(deep.nested.obj.count)描述状态中的目标路径再用O.modify(optic)(fn)在该路径上应用变换函数。与 Immer 不同的是optics-ts 不使用 Proxy也不依赖可变语法整个更新过程是纯函数式的你传入的是一个显式的路径描述与纯函数。使用 Ramda函数式工具库的路径修改还可以使用 RamdaramdaInc: () set(R.modifyPath([deep, nested, obj, count], (c) c 1)),R.modifyPath接收一个路径数组或路径描述和一个变换函数同样以纯函数方式返回更新后的新对象不会原地修改。Ramda 和 optics-ts 都带有完善的类型定义与 TypeScript 配合良好Both ramda and optics-ts also work with types。四种方案对比与选型建议方案代码量实现机制类型支持适用场景手动展开spread冗长逐层...复制原生良好层级浅、更新频率低Immer / immer 中间件极少Proxy 可变 draft自动不可变中间件提供完整推导深层嵌套、复杂不可变数据optics-ts少光学optic路径纯函数完善偏好函数式、需显式路径Ramda少modifyPath纯函数完善已在用 Ramda 的工具函数风格选型提示若项目已经有 Immer 依赖或状态结构很深且更新频繁推荐 immer 中间件——它把produce封装进set无需在每个 action 手动包裹若偏好函数式风格、不希望引入 Proxy 与可变语法optics-ts 与 Ramda 都是干净的选择且两者都支持类型任何情况下都不要忘记set只做一层浅合并深层嵌套必须借助上述任一方案保证不可变性。结合源码理解set合并、替换与更新通知把 docs/learn/guides/updating-state.md 与 immutable-state-and-merging.md 两篇文档对照 src/vanilla.ts 的实现可以得到完整的set行为画像接受对象或函数partial为函数时先以当前state调用得到nextState变更检测Object.is(nextState, state)相等则直接返回不触发任何通知合并 or 替换未传replace默认undefined且nextState为对象 →Object.assign({}, state, nextState)浅合并传了replace true或nextState不是对象 → 直接整体替换通知订阅者listeners.forEach((listener) listener(state, previousState))向所有订阅者广播新状态与旧状态。replace标志的用法来自 immutable-state-and-merging.mdset((state) newState, true)当你需要禁用合并行为、用全新对象整体覆盖状态例如重置 store 或替换整个数据快照时使用。这个标志在 src/vanilla.ts 的类型定义中也有体现replace?: false时允许PartialT而replace: true时要求完整的新状态。延伸阅读immutable-state-and-merging.md浅合并、嵌套对象更新与replace标志的官方说明immer-middleware.mdimmer 中间件的安装、使用与 Gotchas 完整文档src/vanilla.tscreateStore/setState核心实现src/middleware/immer.tsimmer 中间件源码tests/immer.test.tsximmer 中间件行为测试官方在线演示https://stackblitz.com/edit/vitejs-vite-j6bjdyguupdating-state 配套 Demo【免费下载链接】zustand Bear necessities for state management in React项目地址: https://gitcode.com/gh_mirrors/zu/zustand创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
延伸阅读

更多相关文章

2026/9/18 1:21:13

VS Code配置STM32开发环境:ARM GCC+Cortex-Debug全链路指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/18 1:21:13

ADB安卓调试工具详解:安装配置、常用命令与避坑指南

1. ADB到底是个啥,以及装好它的正确姿势1.1 先搞明白:ADB不是“一键刷机”那种黑科技很多刚接触安卓开发或者喜欢折腾手机的朋友,第一次听到“ADB”都会觉得这玩意儿挺神秘。其实说白了,ADB(Android Debug Bridge&…

2026/9/18 1:21:13

Python从Word批量提取校徽并构建FTS5检索与感知哈希去重流水线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/18 7:46:27

Folia Electron架构剖析:主进程、预加载与渲染器分工指南

Folia Electron架构剖析:主进程、预加载与渲染器分工指南 【免费下载链接】folia-major 专注于绚丽的歌词动画效果的本地音乐/navidrome/第三方多平台在线音乐播放器 项目地址: https://gitcode.com/GitHub_Trending/fo/folia-major Folia(folia-…

2026/9/18 7:46:27

AI教材编写中的查重问题与降重技巧

1. 为什么我们需要关注AI教材编写的查重问题去年我在帮某教育机构编写Python入门教材时,遇到了一个尴尬的情况:用AI生成的三个章节内容在查重系统中被标记为27%的相似度。这个数字看起来不高,但其中有整整12%的相似内容集中在对"for循环…

2026/9/18 7:46:27

Flutter表单开发实践:OpenHarmony剧本杀组队应用

1. 项目背景与需求分析在OpenHarmony生态中开发剧本杀组队应用,表单功能是连接玩家需求与组队活动的关键桥梁。这个模块需要解决三个核心问题:如何设计符合剧本杀场景的表单数据结构如何实现跨平台的表单交互体验如何保证表单数据与后端服务的无缝对接我…

2026/9/18 7:41:27

3ds Max真实能力成长路径:从操作到项目交付的五层跃迁

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/16 12:52:37

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/18 0:01:09

Google Colab 实战:运行模型、数据加载与报错排查

1. 为什么我劝你先搞懂 Colab 的运行模型1.1 Colab 到底是什么,跟本地跑代码差在哪Google Colab 简单说就是一台跑在浏览器里的 Linux 虚拟机,你打开一个 Notebook,背后就连上了一台带 GPU 的远程机器。你在单元格里敲的每一行 Python&#x…

2026/9/18 0:01:09

C语言数据类型与表达式详解

1. C语言数据与数据类型概述在C语言编程中,数据是程序处理的核心对象。理解数据的分类和特性是掌握C语言的基础。C语言中的数据主要分为四大类:常量、变量、表达式和函数。这些数据类型构成了C语言程序的基本元素,每种类型都有其独特的特性和…

2026/9/18 0:01:09

SQL时间字段指定时间段查询:区间语义、索引与时区避坑

上周排查一个线上问题&#xff0c;用户反馈"昨天的订单一条都没查到"&#xff0c;但数据库里明明躺着两千多条。最后定位下来&#xff0c;不是数据丢了&#xff0c;也不是接口挂了&#xff0c;而是那个查询条件把时间段写成了> 2024-05-20 00:00:00 AND < 2024…

2026/9/16 22:55:57

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行&#xff0c;Type-C接口算是典型的“看着简单&#xff0c;做起来全坑”的东西。光引脚就24个&#xff0c;高低速信号、电源、控制线全部塞在一个小小的连接器里&#xff0c;如果PCB布局不做规划&#xff0c;打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/16 22:56:09

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/16 22:56:16

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码