You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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实现了原型(修改单个用户名称不会触发其他用户组件重渲染),想知道:

  1. 能否用Preact Signals搭建类似的状态管理方案?
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 12:32:04