store-data-structures by lobehub/lobehub
npx skills add https://github.com/lobehub/lobehub --skill store-data-structures本指南涵盖如何在 Zustand 存储中构建数据以实现最佳性能和用户体验。
Record<string, Detail> 缓存多个详情页面@lobechat/database 类型@lobechat/types 中的类型类型应按实体组织在单独的文件中:
@lobechat/types/src/eval/
├── benchmark.ts # Benchmark 类型
├── agentEvalDataset.ts # 数据集类型
├── agentEvalRun.ts # 运行类型
└── index.ts # 重新导出
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
// packages/types/src/eval/benchmark.ts
import type { EvalBenchmarkRubric } from './rubric';
// ============================================
// 详情类型 - 完整实体(用于详情页面)
// ============================================
/**
* 完整的 benchmark 实体,包含所有字段,包括大量数据
*/
export interface AgentEvalBenchmark {
createdAt: Date;
description?: string | null;
id: string;
identifier: string;
isSystem: boolean;
metadata?: Record<string, unknown> | null;
name: string;
referenceUrl?: string | null;
rubrics: EvalBenchmarkRubric[]; // 大量字段
updatedAt: Date;
}
// ============================================
// 列表类型 - 轻量级(用于列表显示)
// ============================================
/**
* 轻量级 benchmark 项 - 排除大量字段
* 可能包含用于 UI 的计算统计信息
*/
export interface AgentEvalBenchmarkListItem {
createdAt: Date;
description?: string | null;
id: string;
identifier: string;
isSystem: boolean;
name: string;
// 注意:不包含 rubrics(大量字段)
// 用于 UI 显示的计算统计信息
datasetCount?: number;
runCount?: number;
testCaseCount?: number;
}
// packages/types/src/document.ts
/**
* 完整的文档实体 - 包含大量内容字段
*/
export interface Document {
id: string;
title: string;
description?: string;
content: string; // 大量字段 - 完整的 markdown 内容
editorData: any; // 大量字段 - 编辑器状态
metadata?: Record<string, unknown>;
createdAt: Date;
updatedAt: Date;
}
/**
* 轻量级文档项 - 排除大量内容
*/
export interface DocumentListItem {
id: string;
title: string;
description?: string;
// 注意:不包含 content 和 editorData
createdAt: Date;
updatedAt: Date;
// 计算统计信息
wordCount?: number;
lastEditedBy?: string;
}
关键点:
testCaseCount)@lobechat/types 导出,而不是 @lobechat/database从列表中排除的大量字段:
content、editorData、fullDescription)rubrics、config、metrics)image、file)messages、items)✅ 详情页面数据缓存 - 同时缓存多个详情页面 ✅ 乐观更新 - 在 API 响应前更新 UI ✅ 每项加载状态 - 跟踪哪些项正在更新 ✅ 打开多个页面 - 用户可以在详情之间导航而无需重新获取
结构:
benchmarkDetailMap: Record<string, AgentEvalBenchmark>;
示例: Benchmark 详情页面、数据集详情页面、用户资料
✅ 列表显示 - 列表、表格、卡片 ✅ 只读或整体刷新 - 整个列表一起刷新 ✅ 无每项更新 - 无需更新单个项目 ✅ 简单的数据流 - 更容易理解和维护
结构:
benchmarkList: AgentEvalBenchmarkListItem[]
示例: Benchmark 列表、数据集列表、用户列表
// packages/types/src/eval/benchmark.ts
import type { EvalBenchmarkRubric } from './rubric';
/**
* 完整的 benchmark 实体(用于详情页面)
*/
export interface AgentEvalBenchmark {
id: string;
name: string;
description?: string | null;
identifier: string;
rubrics: EvalBenchmarkRubric[]; // 大量字段
metadata?: Record<string, unknown> | null;
isSystem: boolean;
createdAt: Date;
updatedAt: Date;
}
/**
* 轻量级 benchmark(用于列表显示)
* 排除大量字段,如 rubrics
*/
export interface AgentEvalBenchmarkListItem {
id: string;
name: string;
description?: string | null;
identifier: string;
isSystem: boolean;
createdAt: Date;
// 注意:排除 rubrics
// 计算统计信息
testCaseCount?: number;
datasetCount?: number;
runCount?: number;
}
// src/store/eval/slices/benchmark/initialState.ts
import type { AgentEvalBenchmark, AgentEvalBenchmarkListItem } from '@lobechat/types';
export interface BenchmarkSliceState {
// ============================================
// 列表数据 - 简单数组
// ============================================
/**
* 用于列表页面显示的 benchmark 列表
* 可能包含计算字段,如 testCaseCount
*/
benchmarkList: AgentEvalBenchmarkListItem[];
benchmarkListInit: boolean;
// ============================================
// 详情数据 - 用于缓存的 Map
// ============================================
/**
* 按 ID 键控的 benchmark 详情 Map
* 缓存多个 benchmark 的详情页面数据
* 支持乐观更新和每项加载
*/
benchmarkDetailMap: Record<string, AgentEvalBenchmark>;
/**
* 跟踪哪些 benchmark 详情正在加载/更新
* 用于在特定项上显示加载指示器
*/
loadingBenchmarkDetailIds: string[];
// ============================================
// 变更状态
// ============================================
isCreatingBenchmark: boolean;
isUpdatingBenchmark: boolean;
isDeletingBenchmark: boolean;
}
export const benchmarkInitialState: BenchmarkSliceState = {
benchmarkList: [],
benchmarkListInit: false,
benchmarkDetailMap: {},
loadingBenchmarkDetailIds: [],
isCreatingBenchmark: false,
isUpdatingBenchmark: false,
isDeletingBenchmark: false,
};
// src/store/eval/slices/benchmark/reducer.ts
import { produce } from 'immer';
import type { AgentEvalBenchmark } from '@lobechat/types';
// ============================================
// 操作类型
// ============================================
type SetBenchmarkDetailAction = {
id: string;
type: 'setBenchmarkDetail';
value: AgentEvalBenchmark;
};
type UpdateBenchmarkDetailAction = {
id: string;
type: 'updateBenchmarkDetail';
value: Partial<AgentEvalBenchmark>;
};
type DeleteBenchmarkDetailAction = {
id: string;
type: 'deleteBenchmarkDetail';
};
export type BenchmarkDetailDispatch =
| SetBenchmarkDetailAction
| UpdateBenchmarkDetailAction
| DeleteBenchmarkDetailAction;
// ============================================
// Reducer 函数
// ============================================
export const benchmarkDetailReducer = (
state: Record<string, AgentEvalBenchmark> = {},
payload: BenchmarkDetailDispatch,
): Record<string, AgentEvalBenchmark> => {
switch (payload.type) {
case 'setBenchmarkDetail': {
return produce(state, (draft) => {
draft[payload.id] = payload.value;
});
}
case 'updateBenchmarkDetail': {
return produce(state, (draft) => {
if (draft[payload.id]) {
draft[payload.id] = { ...draft[payload.id], ...payload.value };
}
});
}
case 'deleteBenchmarkDetail': {
return produce(state, (draft) => {
delete draft[payload.id];
});
}
default:
return state;
}
};
// In action.ts
export interface BenchmarkAction {
// ... 其他方法 ...
// 内部方法 - 不直接用于 UI
internal_dispatchBenchmarkDetail: (payload: BenchmarkDetailDispatch) => void;
internal_updateBenchmarkDetailLoading: (id: string, loading: boolean) => void;
}
export const createBenchmarkSlice: StateCreator<...> = (set, get) => ({
// ... 其他方法 ...
// 内部 - 分派到 reducer
internal_dispatchBenchmarkDetail: (payload) => {
const currentMap = get().benchmarkDetailMap;
const nextMap = benchmarkDetailReducer(currentMap, payload);
// 仅在更改时更新
if (isEqual(nextMap, currentMap)) return;
set(
{ benchmarkDetailMap: nextMap },
false,
`dispatchBenchmarkDetail/${payload.type}`,
);
},
// 内部 - 更新加载状态
internal_updateBenchmarkDetailLoading: (id, loading) => {
set(
(state) => {
if (loading) {
return { loadingBenchmarkDetailIds: [...state.loadingBenchmarkDetailIds, id] };
}
return {
loadingBenchmarkDetailIds: state.loadingBenchmarkDetailIds.filter((i) => i !== id),
};
},
false,
'updateBenchmarkDetailLoading',
);
},
});
interface BenchmarkSliceState {
// ❌ 只能缓存一个详情
benchmarkDetail: AgentEvalBenchmark | null;
// ❌ 全局加载状态
isLoadingBenchmarkDetail: boolean;
}
问题:
import type { AgentEvalBenchmark, AgentEvalBenchmarkListItem } from '@lobechat/types';
interface BenchmarkSliceState {
// ✅ 列表数据 - 简单数组
benchmarkList: AgentEvalBenchmarkListItem[];
benchmarkListInit: boolean;
// ✅ 详情数据 - 用于缓存的 Map
benchmarkDetailMap: Record<string, AgentEvalBenchmark>;
// ✅ 每项加载
loadingBenchmarkDetailIds: string[];
// ✅ 变更状态
isCreatingBenchmark: boolean;
isUpdatingBenchmark: boolean;
isDeletingBenchmark: boolean;
}
好处:
const BenchmarkList = () => {
// 简单数组访问
const benchmarks = useEvalStore((s) => s.benchmarkList);
const isInit = useEvalStore((s) => s.benchmarkListInit);
if (!isInit) return <Loading />;
return (
<div>
{benchmarks.map(b => (
<BenchmarkCard
key={b.id}
name={b.name}
testCaseCount={b.testCaseCount} // 计算字段
/>
))}
</div>
);
};
const BenchmarkDetail = () => {
const { benchmarkId } = useParams<{ benchmarkId: string }>();
// 从 Map 中获取
const benchmark = useEvalStore((s) =>
benchmarkId ? s.benchmarkDetailMap[benchmarkId] : undefined,
);
// 检查加载状态
const isLoading = useEvalStore((s) =>
benchmarkId ? s.loadingBenchmarkDetailIds.includes(benchmarkId) : false,
);
if (!benchmark) return <Loading />;
return (
<div>
<h1>{benchmark.name}</h1>
{isLoading && <Spinner />}
</div>
);
};
// src/store/eval/slices/benchmark/selectors.ts
export const benchmarkSelectors = {
getBenchmarkDetail: (id: string) => (s: EvalStore) => s.benchmarkDetailMap[id],
isLoadingBenchmarkDetail: (id: string) => (s: EvalStore) =>
s.loadingBenchmarkDetailIds.includes(id),
};
// 在组件中
const benchmark = useEvalStore(benchmarkSelectors.getBenchmarkDetail(benchmarkId!));
const isLoading = useEvalStore(benchmarkSelectors.isLoadingBenchmarkDetail(benchmarkId!));
Need to store data?
│
├─ Is it a LIST for display?
│ └─ ✅ Use simple array: `xxxList: XxxListItem[]`
│ - May include computed fields
│ - Refreshed as a whole
│ - No optimistic updates needed
│
└─ Is it DETAIL page data?
└─ ✅ Use Map: `xxxDetailMap: Record<string, Xxx>`
- Cache multiple details
- Support optimistic updates
- Per-item loading states
- Requires reducer for mutations
设计存储状态结构时:
benchmark.ts、agentEvalDataset.ts)xxxList: XxxListItem[]xxxDetailMap: Record<string, Xxx>loadingXxxDetailIds: string[]xxxList,Map 用 xxxDetailMapany,始终使用正确的类型❌ 不要扩展 Detail 来创建 List:
// 错误 - List 不应该扩展 Detail
export interface BenchmarkListItem extends Benchmark {
testCaseCount?: number;
}
✅ 创建单独的子集:
// 正确 - List 是包含计算字段的子集
export interface BenchmarkListItem {
id: string;
name: string;
// ... 仅必要的字段
testCaseCount?: number; // 计算字段
}
❌ 不要在一个文件中混合多个实体:
// 错误 - 所有实体都在 agentEvalEntities.ts 中
✅ 按实体分开:
// 正确 - 分开文件
// benchmark.ts
// agentEvalDataset.ts
// agentEvalRun.ts
data-fetching - 如何获取和更新这些数据zustand - 通用的 Zustand 模式每周安装次数
186
仓库
GitHub 星标数
74.1K
首次出现
2026年2月21日
安全审计
安装于
codex185
github-copilot185
gemini-cli184
cursor184
opencode184
amp184
This guide covers how to structure data in Zustand stores for optimal performance and user experience.
Record<string, Detail>@lobechat/database types in stores@lobechat/typesTypes should be organized by entity in separate files:
@lobechat/types/src/eval/
├── benchmark.ts # Benchmark types
├── agentEvalDataset.ts # Dataset types
├── agentEvalRun.ts # Run types
└── index.ts # Re-exports
// packages/types/src/eval/benchmark.ts
import type { EvalBenchmarkRubric } from './rubric';
// ============================================
// Detail Type - Full entity (for detail pages)
// ============================================
/**
* Full benchmark entity with all fields including heavy data
*/
export interface AgentEvalBenchmark {
createdAt: Date;
description?: string | null;
id: string;
identifier: string;
isSystem: boolean;
metadata?: Record<string, unknown> | null;
name: string;
referenceUrl?: string | null;
rubrics: EvalBenchmarkRubric[]; // Heavy field
updatedAt: Date;
}
// ============================================
// List Type - Lightweight (for list display)
// ============================================
/**
* Lightweight benchmark item - excludes heavy fields
* May include computed statistics for UI
*/
export interface AgentEvalBenchmarkListItem {
createdAt: Date;
description?: string | null;
id: string;
identifier: string;
isSystem: boolean;
name: string;
// Note: rubrics NOT included (heavy field)
// Computed statistics for UI display
datasetCount?: number;
runCount?: number;
testCaseCount?: number;
}
// packages/types/src/document.ts
/**
* Full document entity - includes heavy content fields
*/
export interface Document {
id: string;
title: string;
description?: string;
content: string; // Heavy field - full markdown content
editorData: any; // Heavy field - editor state
metadata?: Record<string, unknown>;
createdAt: Date;
updatedAt: Date;
}
/**
* Lightweight document item - excludes heavy content
*/
export interface DocumentListItem {
id: string;
title: string;
description?: string;
// Note: content and editorData NOT included
createdAt: Date;
updatedAt: Date;
// Computed statistics
wordCount?: number;
lastEditedBy?: string;
}
Key Points:
testCaseCount)@lobechat/types, NOT @lobechat/databaseHeavy fields to exclude from List:
content, editorData, fullDescription)rubrics, config, metrics)image, file)messages, items)✅ Detail page data caching - Cache multiple detail pages simultaneously ✅ Optimistic updates - Update UI before API responds ✅ Per-item loading states - Track which items are being updated ✅ Multiple pages open - User can navigate between details without refetching
Structure:
benchmarkDetailMap: Record<string, AgentEvalBenchmark>;
Example: Benchmark detail pages, Dataset detail pages, User profiles
✅ List display - Lists, tables, cards ✅ Read-only or refresh-as-whole - Entire list refreshes together ✅ No per-item updates - No need to update individual items ✅ Simple data flow - Easier to understand and maintain
Structure:
benchmarkList: AgentEvalBenchmarkListItem[]
Example: Benchmark list, Dataset list, User list
// packages/types/src/eval/benchmark.ts
import type { EvalBenchmarkRubric } from './rubric';
/**
* Full benchmark entity (for detail pages)
*/
export interface AgentEvalBenchmark {
id: string;
name: string;
description?: string | null;
identifier: string;
rubrics: EvalBenchmarkRubric[]; // Heavy field
metadata?: Record<string, unknown> | null;
isSystem: boolean;
createdAt: Date;
updatedAt: Date;
}
/**
* Lightweight benchmark (for list display)
* Excludes heavy fields like rubrics
*/
export interface AgentEvalBenchmarkListItem {
id: string;
name: string;
description?: string | null;
identifier: string;
isSystem: boolean;
createdAt: Date;
// Note: rubrics excluded
// Computed statistics
testCaseCount?: number;
datasetCount?: number;
runCount?: number;
}
// src/store/eval/slices/benchmark/initialState.ts
import type { AgentEvalBenchmark, AgentEvalBenchmarkListItem } from '@lobechat/types';
export interface BenchmarkSliceState {
// ============================================
// List Data - Simple Array
// ============================================
/**
* List of benchmarks for list page display
* May include computed fields like testCaseCount
*/
benchmarkList: AgentEvalBenchmarkListItem[];
benchmarkListInit: boolean;
// ============================================
// Detail Data - Map for Caching
// ============================================
/**
* Map of benchmark details keyed by ID
* Caches detail page data for multiple benchmarks
* Enables optimistic updates and per-item loading
*/
benchmarkDetailMap: Record<string, AgentEvalBenchmark>;
/**
* Track which benchmark details are being loaded/updated
* For showing spinners on specific items
*/
loadingBenchmarkDetailIds: string[];
// ============================================
// Mutation States
// ============================================
isCreatingBenchmark: boolean;
isUpdatingBenchmark: boolean;
isDeletingBenchmark: boolean;
}
export const benchmarkInitialState: BenchmarkSliceState = {
benchmarkList: [],
benchmarkListInit: false,
benchmarkDetailMap: {},
loadingBenchmarkDetailIds: [],
isCreatingBenchmark: false,
isUpdatingBenchmark: false,
isDeletingBenchmark: false,
};
// src/store/eval/slices/benchmark/reducer.ts
import { produce } from 'immer';
import type { AgentEvalBenchmark } from '@lobechat/types';
// ============================================
// Action Types
// ============================================
type SetBenchmarkDetailAction = {
id: string;
type: 'setBenchmarkDetail';
value: AgentEvalBenchmark;
};
type UpdateBenchmarkDetailAction = {
id: string;
type: 'updateBenchmarkDetail';
value: Partial<AgentEvalBenchmark>;
};
type DeleteBenchmarkDetailAction = {
id: string;
type: 'deleteBenchmarkDetail';
};
export type BenchmarkDetailDispatch =
| SetBenchmarkDetailAction
| UpdateBenchmarkDetailAction
| DeleteBenchmarkDetailAction;
// ============================================
// Reducer Function
// ============================================
export const benchmarkDetailReducer = (
state: Record<string, AgentEvalBenchmark> = {},
payload: BenchmarkDetailDispatch,
): Record<string, AgentEvalBenchmark> => {
switch (payload.type) {
case 'setBenchmarkDetail': {
return produce(state, (draft) => {
draft[payload.id] = payload.value;
});
}
case 'updateBenchmarkDetail': {
return produce(state, (draft) => {
if (draft[payload.id]) {
draft[payload.id] = { ...draft[payload.id], ...payload.value };
}
});
}
case 'deleteBenchmarkDetail': {
return produce(state, (draft) => {
delete draft[payload.id];
});
}
default:
return state;
}
};
// In action.ts
export interface BenchmarkAction {
// ... other methods ...
// Internal methods - not for direct UI use
internal_dispatchBenchmarkDetail: (payload: BenchmarkDetailDispatch) => void;
internal_updateBenchmarkDetailLoading: (id: string, loading: boolean) => void;
}
export const createBenchmarkSlice: StateCreator<...> = (set, get) => ({
// ... other methods ...
// Internal - Dispatch to reducer
internal_dispatchBenchmarkDetail: (payload) => {
const currentMap = get().benchmarkDetailMap;
const nextMap = benchmarkDetailReducer(currentMap, payload);
// Only update if changed
if (isEqual(nextMap, currentMap)) return;
set(
{ benchmarkDetailMap: nextMap },
false,
`dispatchBenchmarkDetail/${payload.type}`,
);
},
// Internal - Update loading state
internal_updateBenchmarkDetailLoading: (id, loading) => {
set(
(state) => {
if (loading) {
return { loadingBenchmarkDetailIds: [...state.loadingBenchmarkDetailIds, id] };
}
return {
loadingBenchmarkDetailIds: state.loadingBenchmarkDetailIds.filter((i) => i !== id),
};
},
false,
'updateBenchmarkDetailLoading',
);
},
});
interface BenchmarkSliceState {
// ❌ Can only cache one detail
benchmarkDetail: AgentEvalBenchmark | null;
// ❌ Global loading state
isLoadingBenchmarkDetail: boolean;
}
Problems:
import type { AgentEvalBenchmark, AgentEvalBenchmarkListItem } from '@lobechat/types';
interface BenchmarkSliceState {
// ✅ List data - simple array
benchmarkList: AgentEvalBenchmarkListItem[];
benchmarkListInit: boolean;
// ✅ Detail data - map for caching
benchmarkDetailMap: Record<string, AgentEvalBenchmark>;
// ✅ Per-item loading
loadingBenchmarkDetailIds: string[];
// ✅ Mutation states
isCreatingBenchmark: boolean;
isUpdatingBenchmark: boolean;
isDeletingBenchmark: boolean;
}
Benefits:
const BenchmarkList = () => {
// Simple array access
const benchmarks = useEvalStore((s) => s.benchmarkList);
const isInit = useEvalStore((s) => s.benchmarkListInit);
if (!isInit) return <Loading />;
return (
<div>
{benchmarks.map(b => (
<BenchmarkCard
key={b.id}
name={b.name}
testCaseCount={b.testCaseCount} // Computed field
/>
))}
</div>
);
};
const BenchmarkDetail = () => {
const { benchmarkId } = useParams<{ benchmarkId: string }>();
// Get from map
const benchmark = useEvalStore((s) =>
benchmarkId ? s.benchmarkDetailMap[benchmarkId] : undefined,
);
// Check loading
const isLoading = useEvalStore((s) =>
benchmarkId ? s.loadingBenchmarkDetailIds.includes(benchmarkId) : false,
);
if (!benchmark) return <Loading />;
return (
<div>
<h1>{benchmark.name}</h1>
{isLoading && <Spinner />}
</div>
);
};
// src/store/eval/slices/benchmark/selectors.ts
export const benchmarkSelectors = {
getBenchmarkDetail: (id: string) => (s: EvalStore) => s.benchmarkDetailMap[id],
isLoadingBenchmarkDetail: (id: string) => (s: EvalStore) =>
s.loadingBenchmarkDetailIds.includes(id),
};
// In component
const benchmark = useEvalStore(benchmarkSelectors.getBenchmarkDetail(benchmarkId!));
const isLoading = useEvalStore(benchmarkSelectors.isLoadingBenchmarkDetail(benchmarkId!));
Need to store data?
│
├─ Is it a LIST for display?
│ └─ ✅ Use simple array: `xxxList: XxxListItem[]`
│ - May include computed fields
│ - Refreshed as a whole
│ - No optimistic updates needed
│
└─ Is it DETAIL page data?
└─ ✅ Use Map: `xxxDetailMap: Record<string, Xxx>`
- Cache multiple details
- Support optimistic updates
- Per-item loading states
- Requires reducer for mutations
When designing store state structure:
benchmark.ts, agentEvalDataset.ts)xxxList: XxxListItem[]xxxDetailMap: Record<string, Xxx>loadingXxxDetailIds: string[]xxxList for arrays, xxxDetailMap for mapsany, always use proper types❌ DON'T extend Detail in List:
// Wrong - List should not extend Detail
export interface BenchmarkListItem extends Benchmark {
testCaseCount?: number;
}
✅ DO create separate subset:
// Correct - List is a subset with computed fields
export interface BenchmarkListItem {
id: string;
name: string;
// ... only necessary fields
testCaseCount?: number; // Computed
}
❌ DON'T mix entities in one file:
// Wrong - all entities in agentEvalEntities.ts
✅ DO separate by entity:
// Correct - separate files
// benchmark.ts
// agentEvalDataset.ts
// agentEvalRun.ts
data-fetching - How to fetch and update this datazustand - General Zustand patternsWeekly Installs
186
Repository
GitHub Stars
74.1K
First Seen
Feb 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex185
github-copilot185
gemini-cli184
cursor184
opencode184
amp184
Tailwind CSS v4 + shadcn/ui 生产级技术栈配置指南与最佳实践
2,600 周安装
pytest-coverage:Python测试覆盖率工具,一键生成代码覆盖率报告
8,300 周安装
Markdown转HTML专业技能 - 使用marked.js、Pandoc和Hugo实现高效文档转换
8,200 周安装
GitHub Copilot 技能模板制作指南 - 创建自定义 Agent Skills 分步教程
8,200 周安装
ImageMagick图像处理技能:批量调整大小、格式转换与元数据提取
8,200 周安装
GitHub Actions 工作流规范创建指南:AI优化模板与CI/CD流程设计
8,200 周安装
GitHub Copilot SDK 官方开发包 - 在应用中嵌入AI智能体工作流(Python/TypeScript/Go/.NET)
8,200 周安装