store-data-structures
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseLobeHub Store Data Structures
LobeHub Store数据结构
This guide covers how to structure data in Zustand stores for optimal performance and user experience.
本指南介绍如何在Zustand Store中构建数据,以实现最佳性能和用户体验。
Core Principles
核心原则
✅ DO
✅ 推荐做法
- Separate List and Detail - Use different structures for list pages and detail pages
- Use Map for Details - Cache multiple detail pages with
Record<string, Detail> - Use Array for Lists - Simple arrays for list display
- Types from @lobechat/types - Never use types in stores
@lobechat/database - Distinguish List and Detail types - List types may have computed UI fields
- 分离列表与详情数据结构 - 为列表页和详情页使用不同的数据结构
- 使用Map存储详情数据 - 用缓存多个详情页数据
Record<string, Detail> - 使用数组存储列表数据 - 用简单数组实现列表展示
- 类型来源@lobechat/types - 切勿在Store中使用的类型
@lobechat/database - 区分列表与详情类型 - 列表类型可包含UI计算字段
❌ DON'T
❌ 禁止做法
- Don't use single detail object - Can't cache multiple pages
- Don't mix List and Detail types - They have different purposes
- Don't use database types - Use types from
@lobechat/types - Don't use Map for lists - Simple arrays are sufficient
- 不要使用单个详情对象 - 无法缓存多个页面数据
- 不要混合列表与详情类型 - 二者用途不同
- 不要使用数据库类型 - 请使用中的类型
@lobechat/types - 不要用Map存储列表数据 - 简单数组已足够
Type Definitions
类型定义
Types 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类型应按实体划分,存放在单独文件中:
@lobechat/types/src/eval/
├── benchmark.ts # Benchmark types
├── agentEvalDataset.ts # Dataset types
├── agentEvalRun.ts # Run types
└── index.ts # Re-exportsExample: Benchmark Types
示例:基准测试类型
typescript
// 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;
}typescript
// 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;
}Example: Document Types (with heavy content)
示例:文档类型(包含大体积内容)
typescript
// 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:
- Detail types include ALL fields from database (full entity)
- List types are subsets that exclude heavy/large fields
- List types may add computed statistics for UI (e.g., )
testCaseCount - Each entity gets its own file (not mixed together)
- All types exported from , NOT
@lobechat/types@lobechat/database
Heavy fields to exclude from List:
- Large text content (,
content,editorData)fullDescription - Complex objects (,
rubrics,config)metrics - Binary data (,
image)file - Large arrays (,
messages)items
typescript
// 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;
}核心要点:
- 详情类型包含数据库中的所有字段(完整实体)
- 列表类型是详情类型的子集,排除大体积/重型字段
- 列表类型可添加UI用的计算统计字段(如)
testCaseCount - 每个实体对应单独文件(不混合存放)
- 所有类型从导出,而非
@lobechat/types@lobechat/database
需从列表类型中排除的重型字段:
- 大文本内容(、
content、editorData)fullDescription - 复杂对象(、
rubrics、config)metrics - 二进制数据(、
image)file - 大型数组(、
messages)items
When to Use Map vs Array
Map与Array的适用场景
Use Map + Reducer (for Detail Data)
使用Map+Reducer(存储详情数据)
✅ 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:
typescript
benchmarkDetailMap: Record<string, AgentEvalBenchmark>;Example: Benchmark detail pages, Dataset detail pages, User profiles
✅ 详情页数据缓存 - 同时缓存多个详情页数据
✅ 乐观更新 - 在API响应前更新UI
✅ 单条数据加载状态 - 跟踪哪些数据正在更新
✅ 多页面同时打开 - 用户可在详情页间切换,无需重新获取数据
结构:
typescript
benchmarkDetailMap: Record<string, AgentEvalBenchmark>;示例: 基准测试详情页、数据集详情页、用户资料
Use Simple Array (for List Data)
使用简单数组(存储列表数据)
✅ 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:
typescript
benchmarkList: AgentEvalBenchmarkListItem[]Example: Benchmark list, Dataset list, User list
✅ 列表展示 - 列表、表格、卡片展示
✅ 只读或整体刷新 - 整个列表统一刷新
✅ 无需单条数据更新 - 不需要更新单个列表项
✅ 简单数据流 - 更易于理解和维护
结构:
typescript
benchmarkList: AgentEvalBenchmarkListItem[]示例: 基准测试列表、数据集列表、用户列表
State Structure Pattern
状态结构模式
Complete Example
完整示例
typescript
// 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;
}typescript
// 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,
};typescript
// 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;
}typescript
// 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,
};Reducer Pattern (for Detail Map)
Reducer模式(用于详情Map)
Why Use Reducer?
为什么使用Reducer?
- Immutable updates - Immer ensures immutability
- Type-safe actions - TypeScript discriminated unions
- Testable - Pure functions easy to test
- Reusable - Same reducer for optimistic updates and server data
- 不可变更新 - Immer确保数据不可变性
- 类型安全的操作 - TypeScript可区分联合类型
- 可测试性 - 纯函数易于测试
- 可复用性 - 同一Reducer可用于乐观更新和服务器数据处理
Reducer Structure
Reducer结构
typescript
// 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;
}
};typescript
// 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;
}
};Internal Dispatch Methods
内部调度方法
typescript
// 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',
);
},
});typescript
// 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',
);
},
});Data Structure Comparison
数据结构对比
❌ WRONG - Single Detail Object
❌ 错误示例 - 单个详情对象
typescript
interface BenchmarkSliceState {
// ❌ Can only cache one detail
benchmarkDetail: AgentEvalBenchmark | null;
// ❌ Global loading state
isLoadingBenchmarkDetail: boolean;
}Problems:
- Can only cache one detail page at a time
- Switching between details causes unnecessary refetches
- No optimistic updates
- No per-item loading states
typescript
interface BenchmarkSliceState {
// ❌ 只能缓存一个详情页数据
benchmarkDetail: AgentEvalBenchmark | null;
// ❌ 全局加载状态
isLoadingBenchmarkDetail: boolean;
}问题:
- 一次只能缓存一个详情页数据
- 切换详情页会导致不必要的重新获取
- 不支持乐观更新
- 无法跟踪单条数据的加载状态
✅ CORRECT - Separate List and Detail
✅ 正确示例 - 分离列表与详情
typescript
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:
- Cache multiple detail pages
- Fast navigation between cached details
- Optimistic updates with reducer
- Per-item loading states
- Clear separation of concerns
typescript
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;
}优势:
- 可缓存多个详情页数据
- 缓存的详情页间切换速度快
- 结合Reducer支持乐观更新
- 可跟踪单条数据的加载状态
- 关注点清晰分离
Component Usage
组件使用示例
Accessing List Data
获取列表数据
typescript
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>
);
};typescript
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>
);
};Accessing Detail Data
获取详情数据
typescript
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>
);
};typescript
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>
);
};Using Selectors (Recommended)
使用选择器(推荐)
typescript
// 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!));typescript
// 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!));Decision Tree
决策树
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需要存储数据?
│
├─ 是否为展示用列表?
│ └─ ✅ 使用简单数组:`xxxList: XxxListItem[]`
│ - 可包含计算字段
│ - 整体刷新
│ - 无需乐观更新
│
└─ 是否为详情页数据?
└─ ✅ 使用Map:`xxxDetailMap: Record<string, Xxx>`
- 缓存多个详情页
- 支持乐观更新
- 跟踪单条数据加载状态
- 需要Reducer处理操作Checklist
检查清单
When designing store state structure:
- Organize types by entity in separate files (e.g., ,
benchmark.ts)agentEvalDataset.ts - Create Detail type (full entity with all fields including heavy ones)
- Create ListItem type:
- Subset of Detail type (exclude heavy fields)
- May include computed statistics for UI
- NOT extending Detail type (it's a subset, not extension)
- Use array for list data:
xxxList: XxxListItem[] - Use Map for detail data:
xxxDetailMap: Record<string, Xxx> - Add per-item loading:
loadingXxxDetailIds: string[] - Create reducer for detail map if optimistic updates needed
- Add internal dispatch and loading methods
- Create selectors for clean access (optional but recommended)
- Document in comments:
- What fields are excluded from List and why
- What computed fields mean
- What each Map is for
设计Store状态结构时:
- 按实体组织类型 存放在单独文件中(如、
benchmark.ts)agentEvalDataset.ts - 创建详情类型(包含所有字段,包括重型字段的完整实体)
- 创建列表项类型:
- 是详情类型的子集(排除重型字段)
- 可包含UI用的计算统计字段
- 不要继承详情类型(是子集而非扩展)
- 使用数组存储列表数据:
xxxList: XxxListItem[] - 使用Map存储详情数据:
xxxDetailMap: Record<string, Xxx> - 添加单条数据加载状态:
loadingXxxDetailIds: string[] - 若需乐观更新,为详情Map创建Reducer
- 添加内部调度和加载状态更新方法
- 创建选择器封装数据获取逻辑(可选但推荐)
- 添加注释说明:
- 列表类型排除了哪些字段及原因
- 计算字段的含义
- 每个Map的用途
Best Practices
最佳实践
- File organization - One entity per file, not mixed together
- List is subset - ListItem excludes heavy fields, not extends Detail
- Clear naming - for arrays,
xxxListfor mapsxxxDetailMap - Consistent patterns - All detail maps follow same structure
- Type safety - Never use , always use proper types
any - Document exclusions - Comment which fields are excluded from List and why
- Selectors - Encapsulate access patterns
- Loading states - Per-item for details, global for lists
- Immutability - Use Immer in reducers
- 文件组织 - 每个实体对应一个文件,不混合存放
- 列表是子集 - 列表项排除重型字段,不继承详情类型
- 命名清晰 - 数组用,Map用
xxxListxxxDetailMap - 模式统一 - 所有详情Map遵循相同结构
- 类型安全 - 绝不使用,始终使用正确类型
any - 注释排除字段 - 说明列表类型排除了哪些字段及原因
- 选择器 - 封装数据获取模式
- 加载状态 - 详情页用单条数据加载状态,列表用全局加载状态
- 不可变性 - 在Reducer中使用Immer
Common Mistakes to Avoid
需避免的常见错误
❌ DON'T extend Detail in List:
typescript
// Wrong - List should not extend Detail
export interface BenchmarkListItem extends Benchmark {
testCaseCount?: number;
}✅ DO create separate subset:
typescript
// 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:
typescript
// Wrong - all entities in agentEvalEntities.ts✅ DO separate by entity:
typescript
// Correct - separate files
// benchmark.ts
// agentEvalDataset.ts
// agentEvalRun.ts❌ 不要让列表类型继承详情类型:
typescript
// Wrong - List should not extend Detail
export interface BenchmarkListItem extends Benchmark {
testCaseCount?: number;
}✅ 正确做法:创建独立的子集类型
typescript
// Correct - List is a subset with computed fields
export interface BenchmarkListItem {
id: string;
name: string;
// ... only necessary fields
testCaseCount?: number; // Computed
}❌ 不要在一个文件中混合多个实体:
typescript
// Wrong - all entities in agentEvalEntities.ts✅ 正确做法:按实体拆分文件
typescript
// Correct - separate files
// benchmark.ts
// agentEvalDataset.ts
// agentEvalRun.tsRelated Skills
相关技能
- - How to fetch and update this data
data-fetching - - General Zustand patterns
zustand
- - 如何获取和更新此类数据
data-fetching - - 通用Zustand模式
zustand