| name | store-data-structures |
| description | LobeHub Zustand store data-shape patterns. Use when designing store state, list/detail splits, normalized maps, reducers, messagesMap, topicsMap, or choosing shared type sources. |
| user-invocable | false |
LobeHub Store Data Structures
How to structure data in Zustand stores for fast list rendering, multi-detail caching, and ergonomic optimistic updates.
Core Principles
โ
DO
- Separate List and Detail โ 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 a 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
Each entity gets its own file under @lobechat/types/. Each file exports two types:
- Detail type โ full entity, including heavy fields (rubrics, content, editor state, โฆ)
- List item type โ a subset that excludes heavy fields, may add computed UI fields (counts, timestamps formatted for display)
Important: the List type is a subset, not an extends of Detail. Extending pulls the heavy fields right back in.
See references/types.md for full worked examples (Benchmark, Document) and the heavy-field exclusion checklist.
When to Use Map vs Array
Use Map + Reducer โ for Detail Data
โ
Detail page data caching โ multiple detail pages cached simultaneously
โ
Optimistic updates โ update UI before API responds
โ
Per-item loading states โ track which items are being updated
โ
Multi-page navigation โ user can switch between details without refetching
benchmarkDetailMap: Record<string, AgentEvalBenchmark>;
Examples: benchmark detail pages, dataset detail pages, user profiles.
Use Simple Array โ for List Data
โ
List display โ lists, tables, cards
โ
Refresh as a whole โ entire list refreshes together
โ
No per-item updates โ no need to mutate individual rows in place
โ
Simple data flow โ fewer moving parts
benchmarkList: AgentEvalBenchmarkListItem[];
Examples: benchmark list, dataset list, user list.
State Structure Pattern
import type { AgentEvalBenchmark, AgentEvalBenchmarkListItem } from '@lobechat/types';
export interface BenchmarkSliceState {
benchmarkList: AgentEvalBenchmarkListItem[];
benchmarkListInit: boolean;
benchmarkDetailMap: Record<string, AgentEvalBenchmark>;
loadingBenchmarkDetailIds: string[];
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)
When the Detail Map needs optimistic updates (i.e. the user edits a row and the UI should reflect it before the server confirms), wire a typed reducer instead of inlining set calls. This keeps mutations testable and the dispatch surface small.
See references/reducer.md for the full discriminated-union action types, the produce-based reducer, and the internal_dispatch* slice methods that connect them to Zustand.
Data Structure Comparison
โ WRONG โ Single Detail Object
interface BenchmarkSliceState {
benchmarkDetail: AgentEvalBenchmark | null;
isLoadingBenchmarkDetail: boolean;
}
Problems:
- Can only cache one detail page at a time
- Switching between details forces refetch
- No optimistic updates
- No per-item loading states
โ
CORRECT โ Separate List and Detail
interface BenchmarkSliceState {
benchmarkList: AgentEvalBenchmarkListItem[];
benchmarkListInit: boolean;
benchmarkDetailMap: Record<string, AgentEvalBenchmark>;
loadingBenchmarkDetailIds: string[];
isCreatingBenchmark: boolean;
isUpdatingBenchmark: boolean;
isDeletingBenchmark: boolean;
}
Benefits:
- Cache multiple detail pages
- Fast navigation between cached details
- Optimistic updates via reducer
- Per-item loading states
- Clear separation of concerns
Component Usage
Accessing List Data
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>
);
};
Accessing Detail Data
const BenchmarkDetail = () => {
const { benchmarkId } = useParams<{ benchmarkId: string }>();
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>
);
};
Using Selectors (Recommended)
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!));
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
Checklist
When designing store state structure:
Best Practices
- File organization โ one entity per file, not mixed
- List is a subset โ ListItem excludes heavy fields, does not
extends Detail
- Clear naming โ
xxxList for arrays, xxxDetailMap for maps
- Consistent patterns โ all detail maps follow the same shape
- Type safety โ never use
any, always use proper types
- Document exclusions โ comment which fields are excluded and why
- Selectors โ encapsulate access patterns
- Loading states โ per-item for details, global for mutations
- Immutability โ use Immer in reducers
Common Mistakes to Avoid
โ DON'T extend Detail in List:
export interface BenchmarkListItem extends Benchmark {
testCaseCount?: number;
}
โ
DO create separate subset:
export interface BenchmarkListItem {
id: string;
name: string;
testCaseCount?: number;
}
โ 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-architecture โ how to fetch and update this data
zustand โ general Zustand patterns