在 Zustand 中实现 computed 的方式

注:本文结合本人真实项目实践经验,经过 AI 润色。

引言

在状态管理领域,计算属性(computed properties)是一个极其重要的概念。MobX 和 Pinia 等库都内置了计算属性功能,允许开发者声明式地定义派生状态。虽然 Zustand 本身没有直接提供 computed API,但这并不意味着我们无法实现类似的功能。

本文将介绍三种在 Zustand 中实现计算属性的优雅方式,包含官方推荐等方案。

方案一:derive-zustand

https://github.com/zustandjs/derive-zustand 是 Zustand 官方维护的派生状态库,它提供了一种响应式的方式来处理计算逻辑。

核心优势

  • 响应式更新:自动追踪依赖,当依赖状态变化时自动更新
  • 类型安全:完美支持 TypeScript 类型推断
  • 性能优化:避免不必要的重新计算

基本用法

ts 复制代码
import { create, useStore } from 'zustand';
import { derive } from 'derive-zustand';

// 基础 store
const useCountStore = create<{ count: number; inc: () => void }>((set) => ({
  count: 0,
  inc: () => set((state) => ({ count: state.count + 1 })),
}));

// 派生 store
const doubleCountStore = derive<number>((get) => get(useCountStore).count * 2);

// 自定义 hook
const useDoubleCountStore = () => useStore(doubleCountStore);

// 组件中使用
const Counter = () => {
  const { count, inc } = useCountStore();
  const doubleCount = useDoubleCountStore();
  return (
    <div>
      <div>count: {count}</div>
      <div>doubleCount: {doubleCount}</div>
      <button type="button" onClick={inc}>
        +1
      </button>
    </div>
  );
};

适用场景

  • 需要多个组件共享的派生状态
  • 复杂的计算逻辑需要复用
  • 希望保持响应式更新的特性

方案二:手动维护计算属性

对于简单的计算需求,可以直接在 store 中声明派生状态,并在相关操作后手动更新。

实现模式

ts 复制代码
import { create } from 'zustand';

type CartItem = { id: string; price: number; quantity: number };

type CartState = {
  items: CartItem[];
  total: number; // ← 计算属性
  addItem: (item: Omit<CartItem, 'id'>) => void;
  updateTotal: () => void;
};

const useCartStore = create<CartState>((set, get) => ({
  items: [],
  total: 0,

  addItem: (item) => {
    set((state) => ({
      items: [...state.items, { ...item, id: crypto.randomUUID() }],
    }));
    get().updateTotal(); // 添加商品后更新总价
  },

  updateTotal: () => {
    const newTotal = get().items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    );
    set({ total: newTotal });
  },
}));

优缺点分析

优点

  • 不需要额外依赖
  • 逻辑集中,便于维护
  • 更新时机明确可控

缺点

  • 需要手动触发更新
  • 可能遗漏更新点
  • 不适合复杂依赖关系

最佳实践

  • 为计算属性添加专门的更新方法
  • 在文档中明确标注哪些操作会影响计算属性
  • 考虑使用 immer 简化不可变更新逻辑

方案三:在组件内派生状态

对于简单的、仅限单个组件使用的派生状态,可以直接在组件内部计算。

实现示例

tsx 复制代码
const UserProfile = () => {
  const firstName = useUserStore((s) => s.firstName);
  const lastName = useUserStore((s) => s.lastName);

  const fullName = `${firstName} ${lastName}`;

  return (
    <div>{fullName}</div>
  );
};

适用条件

  • 派生状态只在一个组件中使用
  • 计算逻辑非常简单
  • 不需要响应式更新(或可以接受组件重新渲染)

性能考虑

当派生计算较复杂时,可以使用 useMemo 优化:

tsx 复制代码
const expensiveValue = useMemo(() => {
  return computeExpensiveValue(a, b);
}, [a, b]);

方案对比与选择指南

方案 适用场景 复杂度 性能 维护性
derive-zustand 多组件共享的复杂派生状态
Store 内维护 简单的全局计算属性
组件内计算 单一组件使用的简单派生 最低 视情况 视情况

选择建议

  1. 优先考虑 derive-zustand,特别是需要响应式更新时
  2. 对于简单场景,Store 内维护更轻量
  3. 组件内计算适合临时性、局部性的简单逻辑

高级技巧:组合使用多种方案

在实际项目中,你可以灵活组合这些方案。例如:

ts 复制代码
// 使用 derive-zustand 创建基础派生状态
const filteredTodosStore = derive<Todo[]>(get => {
  const { todos, filter } = get(useTodoStore);
  return todos.filter(todo => 
    filter === 'all' || 
    (filter === 'completed' && todo.completed) ||
    (filter === 'active' && !todo.completed)
  );
});

// 在组件内进一步派生
const TodoStats = () => {
  const filteredTodos = useStore(filteredTodosStore);
  
  // 组件特有的派生状态
  const completionPercentage = useMemo(() => {
    if (filteredTodos.length === 0) return 0;
    const completed = filteredTodos.filter(t => t.completed).length;
    return Math.round((completed / filteredTodos.length) * 100);
  }, [filteredTodos]);

  return <div>完成度: {completionPercentage}%</div>;
};

总结与最佳实践

在 Zustand 中实现计算属性有多种方式,没有绝对的"最佳"方案,关键是根据具体场景选择最合适的:

  1. 保持简单:不要过度设计,简单的组件内计算可能就足够了
  2. 关注性能:对于昂贵的计算,使用 memoization 技术
  3. 类型安全:充分利用 TypeScript 确保类型正确
  4. 文档说明:明确标注哪些是计算属性及其依赖关系
  5. 测试覆盖:为重要的计算逻辑添加单元测试

Zustand 的灵活性允许你根据项目需求选择最适合的计算属性实现方式,这种设计哲学正是它受到开发者喜爱的原因之一。

相关推荐
今天不要写bug9 小时前
antv x6实现封装拖拽流程图配置(适用于工单流程、审批流程应用场景)
前端·typescript·vue·流程图
烛阴10 小时前
TypeScript 类型魔法:像遍历对象一样改造你的类型
前端·javascript·typescript
拜晨19 小时前
类型体操的实践与总结: 从useInfiniteScroll 到 InfiniteList
前端·typescript
前端搬运侠21 小时前
🚀 TypeScript 中的 10 个隐藏技巧,让你的代码更优雅!
前端·typescript
Spider_Man1 天前
React 组件缓存与 KeepAlive 组件打造全攻略 😎
前端·react.js·typescript
葡萄城技术团队1 天前
TypeScript 中 Type 与 Interface 到底该怎么选?吃透这几点再也不纠结
typescript
薛定谔的算法1 天前
类型别名(Type Aliases)与接口(Interfaces):相同与不同
前端·面试·typescript
江湖人称小鱼哥1 天前
主流技术栈 NestJS、TypeScript、Node.js版本使用统计
typescript·node.js·nestjs
烛阴1 天前
解锁 TypeScript 的元编程魔法:从 `extends` 到 `infer` 的条件类型之旅
前端·javascript·typescript
四月_h1 天前
在 Vue 3 + TypeScript 项目中实现主题切换功能
前端·vue.js·typescript