如何用Preact Signals处理嵌套状态?是否兼容Immer?
我希望搭建一套满足以下要求的状态管理方案:
- 支持任意层级的嵌套状态,例如
users[0].vehicle.engine.throttle这样的状态路径是合法的 - 适配任意层级的组件树,例如在某个深层菜单中存在对应上述throttle值的滑块组件
- 组件仅在自身关联的状态发生变化时才重渲染
我了解Preact Signals的设计,但不清楚如何用它处理嵌套状态:目前的理解是,组件访问signal.value时,只要该值变化就会触发重渲染。比如有用户对象:
{ name: "John", age: 34 }
我想在NameDisplay和AgeDisplay组件分别消费name和age,怎么避免修改name时AgeDisplay重渲染?如果是用户列表:
{ users:[ { name: "John", age: 34 }, ... ] }
用单个users signal的话,任意用户变更都会导致所有用户组件重渲染,而我用zustand + immer实现了原型(修改单个用户名称不会触发其他用户组件重渲染),想知道:
- 能否用Preact Signals搭建类似的状态管理方案?
- Preact Signals是否兼容Immer?
原型代码
store.js(嵌套用户列表状态 + Immer深层更新)
import { create } from "zustand"; import { immer } from "zustand/middleware/immer"; export const useUsers = create()( immer((set) => ({ users: [ { name: "John", age: 42, id: "1" }, { name: "Jane", age: 21, id: "2" }, ], renameUser: (id, name) => set((state) => { state.users.find((user) => user.id === id).name = name; }), })) );
User.js(订阅特定用户,仅监听该用户更新)
import React from "react"; import { useUsers } from "./store"; import { useShallow } from "zustand/react/shallow"; export default function User(props) { const { id } = props; const user = useUsers( useShallow((state) => state.users.find((user) => user.id === id)) ); const rename = useUsers((state) => state.renameUser); return ( <div> <h1>{user.name}</h1> <h2>{user.age}</h2> <input type="text" value={user.name} onChange={(e) => rename(id, e.target.value)} /> </div> ); }
App.js(展示用户列表的包装组件)
import React from 'react'; import { useUsers } from './store' import { useShallow } from "zustand/react/shallow"; import User from './User' function App() { const users_ids = useUsers(useShallow((state) => state.users.map((user) => user.id))) return ( <div className="App"> {users_ids.map((id) => <User key={id} id={id} />)} </div> ); } export default App;
一、用Preact Signals实现细粒度状态订阅
Preact Signals支持细粒度的嵌套状态订阅,不需要完全扁平化状态,核心是利用signal的自动依赖追踪或computed实现精准订阅,避免不必要的重渲染。
1. 单个用户对象的处理
方式1:拆分独立Signal
将每个独立字段封装为单独的Signal,组件仅订阅自身依赖的字段:
import { signal } from '@preact/signals'; const userName = signal("John"); const userAge = signal(34); // 仅订阅userName,修改age不会触发重渲染 function NameDisplay() { return <div>{userName.value}</div>; } // 仅订阅userAge,修改name不会触发重渲染 function AgeDisplay() { return <div>{userAge.value}</div>; }
方式2:嵌套对象Signal + Computed精准订阅
用单个Signal包裹完整对象,通过computed提取所需字段,仅追踪该字段的变化:
import { signal, computed } from '@preact/signals'; const user = signal({ name: "John", age: 34 }); function NameDisplay() { // 仅依赖user.value.name,修改age不会触发此组件重渲染 const name = computed(() => user.value.name); return <div>{name.value}</div>; } function AgeDisplay() { const age = computed(() => user.value.age); return <div>{age.value}</div>; }
2. 用户列表的处理
给每个用户对象的独立字段创建Signal,再用一个Signal管理用户列表引用,实现单个用户变更仅触发对应组件重渲染:
import { signal, computed } from '@preact/signals'; // 创建单个用户的Signal结构 function createUser(id, name, age) { return { id: id, name: signal(name), age: signal(age) }; } // 管理用户列表的Signal const users = signal([ createUser("1", "John", 42), createUser("2", "Jane", 21) ]); // 仅修改目标用户的name Signal function renameUser(id, newName) { const user = users.value.find(u => u.id === id); if (user) user.name.value = newName; } // User组件:仅订阅当前用户的name和age Signal function User({ id }) { // Computed仅依赖匹配id的用户引用,其他用户变更不影响 const user = computed(() => users.value.find(u => u.id === id)); return ( <div> <h1>{user.value.name.value}</h1> <h2>{user.value.age.value}</h2> <input type="text" value={user.value.name.value} onChange={(e) => renameUser(id, e.target.value)} /> </div> ); } // App组件:仅订阅用户id列表,用户属性变更不会触发重渲染 function App() { const userIds = computed(() => users.value.map(u => u.id)); return ( <div className="App"> {userIds.value.map(id => <User key={id} id={id} />)} </div> ); }
此方案中,修改单个用户的name时,仅该用户的User组件会重渲染,其他用户组件和App组件均不受影响,与你用zustand实现的效果一致。
二、Preact Signals与Immer的兼容性
Preact Signals完全兼容Immer,可结合Immer的不可变更新语法处理复杂嵌套状态:
import { signal } from '@preact/signals'; import { produce } from 'immer'; const users = signal([ { id: "1", name: "John", age: 42 }, { id: "2", name: "Jane", age: 21 } ]); function renameUser(id, newName) { // 用Immer生成新状态,更新Signal的值 users.value = produce(users.value, draft => { const user = draft.find(u => u.id === id); if (user) user.name = newName; }); }
需注意:此方式是更新整个users Signal的value,若组件直接订阅users.value会触发重渲染。若要实现细粒度重渲染,建议结合前面的方案——将单个用户的属性设为独立Signal,或用computed精准订阅所需字段。
内容的提问来源于stack exchange,提问作者lhk

