mcpField
W
web-state-zustand

web-state-zustand

github.com/agents-inc开发2689 次安装2 天前更新
代码仓库

关于 web-state-zustand

为使用 Zustand 的前端开发者提供状态管理模式,帮助判断状态应放在 Store、组件、URL 还是 Context。内容聚焦选择器、订阅范围及 v5 行为。

主要功能

  1. 01Store 与其他状态位置的选择
  2. 02原子选择器与浅比较
  3. 03使用 devtools 和 persist
  4. 04拆分聚焦型 Store
  5. 05URL 中保存可分享状态

适用场景

  1. 01在多个组件间共享 UI 状态
  2. 02限制状态变化引发的重渲染
  3. 03持久化用户偏好
  4. 04将筛选和分页放入 URL

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/agents-inc/skills --skill web-state-zustand

安装后在 Claude Code 中输入/web-state-zustand确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.yaml
reference.md
examples/
core.md

详细介绍

Zustand 模式

快速指南: 两个或更多组件读取的状态由 Zustand 管理;一个组件读取的状态由 useState 管理;筛选条件和搜索内容由 URL 管理;Context 用于注入单例,不保存状态。Zustand v5 有三项变化:zustand/react/shallow 中的 useShallow 取代旧版作为第二个参数的相等性函数;选择器必须返回稳定引用,否则会陷入循环;persist 不再在创建时捕获初始状态。

详细资源:

  • examples/core.md — store 设置、原子选择器、useShallow、Context 反模式、URL 状态
  • reference.md — 中间件查询、v5 迁移说明、对等依赖要求

<critical_requirements>

编写 Zustand 代码前

只要第二个组件要读取某个值,就立即将它移入 store。 将它逐层传递会让路径上的每个组件都与之耦合,而之后再移动它就意味着要拆解这条传递链。

每次调用 useStore 只选择一个值。 这样,组件仅在该值变化时重新渲染;不带选择器的调用会订阅整个 store。

每个选择器都要返回稳定引用。 在选择器内部创建的对象或函数,每次调用都会产生新引用;在 v5 中,这会导致无休止地重新渲染。

只通过 partialize 持久化偏好设置,不要持久化其他内容。 从存储中恢复的临时 UI 状态——例如会自动重新打开的模态框、会记住关闭状态的侧边栏——看起来就像是 bug。

不要将服务端数据放进 store。 缓存、失效和重新获取数据需要专门为此构建的层;store 只会给你一份悄然过时的数据副本。

</critical_requirements>


自动检测: create from zustand、zustand/middleware、zustand/react/shallow、useShallow、createWithEqualityFn、zustand/traditional、persist、partialize、devtools、store 切片

适用范围:

  • 针对给定值,在 store、useState、URL 和 Context 之间做出选择
  • 使用 devtools 和 persist 设置 store
  • 选择器结构与重新渲染行为
  • 将状态拆分到各个专用 store 中

其他部分介绍:

  • 服务端数据——缓存、失效和重新获取应由负责网络的部分处理,store 并不负责这些
  • 路由机制——本指南说明哪些状态应该放在 URL 中,不涉及特定路由器如何读取或写入这些状态
  • 表单字段状态——表单打开时,表单库负责管理自身的字段值

Zustand 是一种附带 hook 的订阅原语。它的性能完全取决于选择器:store 每次变化都会通知所有订阅者,而选择器决定这次通知是否会触发渲染。因此,store 设计就是选择器设计。

  • 优先选择小型 store,而不是一个大型 store——store 既是订阅单位,也是代码单位
  • 将业务逻辑放在 action 中——组件调用 toggleSidebar();store 决定切换意味着什么
  • 导出 hook,而不是创建器——原始 store 是可变的全局对象,导出它会引来来自任何地方的写入
  • 优先使用原子选择器——单个值完全不需要比较函数

<decision_framework>

值应该放在哪里

值应该放在原因
被 2 个或更多组件读取store可选择性重新渲染,而且以后无需理清层层传递的 props
被 1 个组件读取useState没有其他对象能观察到它,因此也不需要观察它
筛选条件、查询、页面或排序URL可分享、可收藏为书签,而且后退按钮可以正常工作
启动时设置的单例Context它永远不会变化,因此不会产生重新渲染开销
从 API 获取不属于此技能它需要缓存和失效机制,而 store 两者都没有

Context 是人们最容易用错的一项。它用于传递不会变化的值;如果用它管理会变化的状态,每次变化都会重新渲染所有消费者,而且无法选择退出。

</decision_framework>


核心模式

模式 1:Store 设置

使用 devtools 进行检查,使用 persist 持久化那些需要在标签页关闭后保留的字段,并用 partialize 将其他内容排除在存储之外。

export const useUIStore = create<UIState>()(
  devtools(
    persist(
      (set) => ({
        theme: DEFAULT_THEME,
        sidebarOpen: true,
        toggleSidebar: () =>
          set((state) => ({ sidebarOpen: !state.sidebarOpen })),
      }),
      { name: UI_STORAGE_KEY, partialize: (state) => ({ theme: state.theme }) },
    ),
  ),
);

完整代码:examples/core.md

模式 2:原子选择器

每次调用只选取一个值是默认做法,也是 store 保持快速的原因。

const toggleSidebar = useUIStore((state) => state.toggleSidebar);
const isOpen = useUIStore((state) => state.sidebarOpen);

完整代码:examples/core.md

模式 3:使用 useShallow 选取多个值

从同一个 store 选取 3 个或更多值时,一次 useShallow 调用胜过 3 个订阅。

import { useShallow } from "zustand/react/shallow";

const { sidebarOpen, theme } = useUIStore(
  useShallow((state) => ({
    sidebarOpen: state.sidebarOpen,
    theme: state.theme,
  })),
);

完整代码:examples/core.md

模式 4:Context 用于注入,而非状态管理

Context 用于传递永远不会变化的值——客户端、连接,或启动时读取的配置。

const DatabaseContext = createContext<Database | null>(null);

完整代码:examples/core.md

模式 5:将可分享的筛选条件保存在 URL 中

筛选条件、搜索、分页和排序都放在 URL 中,这样重新加载页面或将链接粘贴到同事的聊天窗口后,它们仍然存在。

const searchParams = new URLSearchParams(window.location.search);
const category = searchParams.get("category") ?? undefined;

完整代码:examples/core.md


<red_flags>

注意事项

会在运行时出错:

  • 选择器返回内联对象或函数——每次调用都会产生新的引用,因此 v5 会不停地重新渲染。将回退值提取为模块常量。
  • 使用第二个相等函数参数调用 create()——该参数已在 v5 中移除,因此会被忽略,比较也会悄无声息地失效。请使用 useShallow,或使用 zustand/traditional 中的 createWithEqualityFn。
  • 创建持久化 store 后立即读取其中计算得出的初始值——v5 的 persist 不会存储该值。创建后使用 setState 设置它。

行为可能出乎意料:

  • 不带选择器调用 useStore() 会订阅整个 store,因此组件会因其从未读取的字段发生变化而重新渲染。
  • 两个返回相同值的原子选择器仍会分别触发渲染,只要其中一个发生变化;useShallow 比较的是对象,而不是各个单独读取的值。
  • 每当重建 provider 的值对象时,Context 都会重新渲染所有消费者;除非对该值进行记忆化处理,否则每次渲染都会重建。即使进行了记忆化,只要其中一个字段变化,读取所有字段的组件仍会重新渲染。
  • 搜索参数都是字符串。?page=2 读取后是 "2",而 ?open=false 在解析之前仍会被视为真值。
  • 一个大型 store 意味着庞大的订阅范围:每个从中读取数据的组件都会与每次写入竞争。

</red_flags>

评论

支持 Markdown