store-data-structures

LobeHub Store Data Structures

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "store-data-structures" with this command: npx skills add lobehub/lobehub/lobehub-lobehub-store-data-structures

LobeHub Store Data Structures

This guide covers how to structure data in Zustand stores for optimal performance and user experience.

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 @lobechat/database types in stores

  • Distinguish List and Detail types - List types may have computed UI fields

❌ 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

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

Example: Benchmark Types

// 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)

// 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 )

When to Use Map vs Array

Use Map + Reducer (for Detail Data)

✅ 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

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:

benchmarkList: AgentEvalBenchmarkListItem[]

Example: Benchmark list, Dataset list, User list

State Structure Pattern

Complete Example

// 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, };

Reducer Pattern (for Detail Map)

Why Use 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

Reducer Structure

// 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

// 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

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

✅ CORRECT - Separate List and Detail

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

Component Usage

Accessing List Data

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

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)

// 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&#x3C;string, Xxx> - Cache multiple details - Support optimistic updates - Per-item loading states - Requires reducer for mutations

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

Best Practices

  • File organization - One entity per file, not mixed together

  • List is subset - ListItem excludes heavy fields, not extends Detail

  • Clear naming - xxxList for arrays, xxxDetailMap for maps

  • Consistent patterns - All detail maps follow same structure

  • Type safety - Never use any , always use proper types

  • 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

Common Mistakes to Avoid

❌ 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

Related Skills

  • data-fetching

  • How to fetch and update this data

  • zustand

  • General Zustand patterns

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

react

No summary provided by upstream source.

Repository SourceNeeds Review
-1.3K
lobehub
General

zustand

No summary provided by upstream source.

Repository SourceNeeds Review
General

project-overview

No summary provided by upstream source.

Repository SourceNeeds Review