store-data-structures

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

LobeHub 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

✅ 推荐做法

  1. Separate List and Detail - Use different structures for list pages and detail pages
  2. Use Map for Details - Cache multiple detail pages with
    Record<string, Detail>
  3. Use Array for Lists - Simple arrays for list display
  4. Types from @lobechat/types - Never use
    @lobechat/database
    types in stores
  5. Distinguish List and Detail types - List types may have computed UI fields
  1. 分离列表与详情数据结构 - 为列表页和详情页使用不同的数据结构
  2. 使用Map存储详情数据 - 用
    Record<string, Detail>
    缓存多个详情页数据
  3. 使用数组存储列表数据 - 用简单数组实现列表展示
  4. 类型来源@lobechat/types - 切勿在Store中使用
    @lobechat/database
    的类型
  5. 区分列表与详情类型 - 列表类型可包含UI计算字段

❌ DON'T

❌ 禁止做法

  1. Don't use single detail object - Can't cache multiple pages
  2. Don't mix List and Detail types - They have different purposes
  3. Don't use database types - Use types from
    @lobechat/types
  4. Don't use Map for lists - Simple arrays are sufficient

  1. 不要使用单个详情对象 - 无法缓存多个页面数据
  2. 不要混合列表与详情类型 - 二者用途不同
  3. 不要使用数据库类型 - 请使用
    @lobechat/types
    中的类型
  4. 不要用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-exports

Example: 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
    @lobechat/types
    , NOT
    @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

最佳实践

  1. File organization - One entity per file, not mixed together
  2. List is subset - ListItem excludes heavy fields, not extends Detail
  3. Clear naming -
    xxxList
    for arrays,
    xxxDetailMap
    for maps
  4. Consistent patterns - All detail maps follow same structure
  5. Type safety - Never use
    any
    , always use proper types
  6. Document exclusions - Comment which fields are excluded from List and why
  7. Selectors - Encapsulate access patterns
  8. Loading states - Per-item for details, global for lists
  9. Immutability - Use Immer in reducers
  1. 文件组织 - 每个实体对应一个文件,不混合存放
  2. 列表是子集 - 列表项排除重型字段,不继承详情类型
  3. 命名清晰 - 数组用
    xxxList
    ,Map用
    xxxDetailMap
  4. 模式统一 - 所有详情Map遵循相同结构
  5. 类型安全 - 绝不使用
    any
    ,始终使用正确类型
  6. 注释排除字段 - 说明列表类型排除了哪些字段及原因
  7. 选择器 - 封装数据获取模式
  8. 加载状态 - 详情页用单条数据加载状态,列表用全局加载状态
  9. 不可变性 - 在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.ts

Related Skills

相关技能

  • data-fetching
    - How to fetch and update this data
  • zustand
    - General Zustand patterns
  • data-fetching
    - 如何获取和更新此类数据
  • zustand
    - 通用Zustand模式