引言
在状态管理领域,计算属性(computed properties)是一个极其重要的概念。MobX 和 Pinia 等库都内置了计算属性功能,允许开发者声明式地定义派生状态。虽然 Zustand 本身没有直接提供 computed API,但这并不意味着我们无法实现类似的功能。
本文将介绍三种在 Zustand 中实现计算属性的优雅方式,包含官方推荐等方案。
方案一:derive-zustand
https://github.com/zustandjs/derive-zustand。
核心优势
- 响应式更新:自动追踪依赖,当依赖状态变化时自动更新
- 类型安全:完美支持 TypeScript 类型推断
- 性能优化:避免不必要的重新计算
基本用法
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 中声明派生状态,并在相关操作后手动更新。
实现模式
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简化不可变更新逻辑
方案三:在组件内派生状态
对于简单的、仅限单个组件使用的派生状态,可以直接在组件内部计算。
实现示例
const UserProfile = () => {
const firstName = useUserStore((s) => s.firstName);
const lastName = useUserStore((s) => s.lastName);
const fullName = `${firstName} ${lastName}`;
return (
<div>{fullName}</div>
);
};
适用条件
- 派生状态只在一个组件中使用
- 计算逻辑非常简单
- 不需要响应式更新(或可以接受组件重新渲染)
性能考虑
当派生计算较复杂时,可以使用 useMemo 优化:
const expensiveValue = useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]);
方案对比与选择指南
| 方案 | 适用场景 | 复杂度 | 性能 | 维护性 |
|---|---|---|---|---|
| derive-zustand | 多组件共享的复杂派生状态 | 中 | 优 | 优 |
| Store 内维护 | 简单的全局计算属性 | 低 | 良 | 中 |
| 组件内计算 | 单一组件使用的简单派生 | 最低 | 视情况 | 视情况 |
选择建议:
- 优先考虑 derive-zustand,特别是需要响应式更新时
- 对于简单场景,Store 内维护更轻量
- 组件内计算适合临时性、局部性的简单逻辑
高级技巧:组合使用多种方案
在实际项目中,你可以灵活组合这些方案。例如:
// 使用 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>;
};
3010

被折叠的 条评论
为什么被折叠?



