如何在SolidJS组件中用Immer的produce处理嵌套状态
SolidJS + Immer 嵌套子集修改方案(带撤销重做)
核心思路
不用Proxy,而是让createImmerSignal的createNested方法返回带修改能力的信号包装器——既可以直接读取子集数据,又能通过明确的路径/更新逻辑触发Immer对根对象的修改,同时保留类型安全与撤销重做能力。
实现步骤
1. 基础版:手动指定反向更新逻辑
先实现最直观的版本,通过选择器和反向更新函数明确子集与根对象的关联:
import { produce } from "immer"; import { createSignal, Setter } from "solid-js"; // 定义ImmerSignal类型,包含嵌套创建方法 type ImmerSignal<T> = [ get: () => T, set: Setter<T>, createNested: <K>( selector: (root: T) => K, updater: (root: T, newValue: K) => void ) => [get: () => K, set: Setter<K>] ]; function createImmerSignal<T>(initialValue: T): ImmerSignal<T> { const [state, setState] = createSignal(initialValue); const undoStack: T[] = []; const redoStack: T[] = []; // 根状态更新方法,集成Immer与历史栈 const set: Setter<T> = (updater) => { undoStack.push(state()); redoStack.length = 0; setState((prev) => produce(prev, (draft) => { typeof updater === "function" ? updater(draft) : Object.assign(draft, updater); }) ); }; // 创建嵌套信号的核心方法 const createNested = <K>( selector: (root: T) => K, updater: (root: T, newValue: K) => void ): [() => K, Setter<K>] => { // 读取子集:直接通过选择器从根状态获取 const getNested = () => selector(state()); // 修改子集:触发根状态更新,用Immer处理嵌套修改 const setNested: Setter<K> = (newValue) => { set((root) => { produce(root, (draft) => { if (typeof newValue === "function") { // 支持函数式更新,直接修改子集实例 newValue(selector(draft)); } else { // 替换子集实例 updater(draft, newValue); } }); }); }; return [getNested, setNested]; }; return [state, set, createNested]; }
2. 类型安全的使用示例
假设你的数据结构是嵌套类实例:
class Developer { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } } class Company { name: string; developers: Developer[]; constructor(name: string, developers: Developer[]) { this.name = name; this.developers = developers; } } // 初始化根信号 const initialCompany = new Company("Acme", [new Developer("Alice", 30)]); const [company, setCompany, createNested] = createImmerSignal(initialCompany); // 创建指向第一个开发者的嵌套信号 const [firstDev, setFirstDev] = createNested( (c) => c.developers[0], (c, newDev) => { c.developers[0] = newDev; } ); // 子组件中直接使用 // 读取数据 console.log(firstDev().name); // 输出 "Alice" // 修改数据(替换实例) setFirstDev(new Developer("Bob", 28)); // 函数式更新(修改实例属性) setFirstDev((prev) => { prev.age += 1; });
3. 优化版:自动路径解析(避免手动写updater)
如果不想每次都写反向更新函数,可以用字符串路径代替选择器,通过辅助函数自动解析路径修改根对象:
// 辅助工具:根据路径读取/设置对象属性 function getPath<T>(obj: T, path: string): any { return path.split(".").reduce((acc, key) => acc[key], obj); } function setPath<T>(obj: T, path: string, value: any): void { const keys = path.split("."); const lastKey = keys.pop()!; const parent = keys.reduce((acc, key) => acc[key], obj); parent[lastKey] = value; } // 修改createNested方法,支持路径字符串 const createNested = <K>(path: string): [() => K, Setter<K>] => { const getNested = () => getPath(state(), path) as K; const setNested: Setter<K> = (newValue) => { set((root) => produce(root, (draft) => { if (typeof newValue === "function") { newValue(getPath(draft, path)); } else { setPath(draft, path, newValue); } })); }; return [getNested, setNested]; }; // 使用方式更简洁,配合TS类型工具可实现路径校验 const [firstDev, setFirstDev] = createNested<Developer>("developers.0");
4. 集成撤销重做
所有修改都通过根信号的set方法触发,因此只需在createImmerSignal中添加历史栈管理:
// 在createImmerSignal中添加undo/redo方法 const undo = () => { if (undoStack.length === 0) return; const prevState = undoStack.pop()!; redoStack.push(state()); setState(prevState); }; const redo = () => { if (redoStack.length === 0) return; const nextState = redoStack.pop()!; undoStack.push(state()); setState(nextState); }; // 更新ImmerSignal类型 type ImmerSignal<T> = [ get: () => T, set: Setter<T>, createNested: <K>(path: string) => [get: () => K, set: Setter<K>], undo: () => void, redo: () => void ]; // 返回时包含undo/redo return [state, set, createNested, undo, redo];
方案优势
- 性能高效:无需Proxy的隐式追踪,直接通过路径或更新函数修改,性能损耗极小
- 类型安全:选择器或路径字符串可通过TS类型校验,确保子集存在且类型正确
- 逻辑清晰:修改路径明确,调试时能直接定位到根对象的修改点
- 兼容类实例:依托Immer的能力,完美支持嵌套类实例的修改
内容的提问来源于stack exchange,提问作者Basil
相关产品推荐
相关产品推荐

