LobeHub Store Data Structures
SkillCommunicationLets your agent design and structure app state stores using list/detail splits, normalized maps, and reducers.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the LobeHub Store Data Structures skill
About this capability
LobeHub Zustand store data-shape patterns. Use when designing store state, list/detail splits, normalized maps, reducers, messagesMap, topicsMap, or choosing shared type sources.
What this skill tells your AI
The instructions your AI receives, as published by lobehub/lobehub in .agents/skills/store-data-structures/SKILL.md and read by ahel’s review.
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/databasetypes 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.mdfor 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
// src/store/eval/slices/benchmark/initialState.ts
import type { AgentEvalBenchmark, AgentEvalBenchmarkListItem } from '@lobechat/types';
export interface BenchmarkSliceState {
// List — simple array
benchmarkList: AgentEvalBenchmarkListItem[];
benchmarkListInit: boolean;
// Detail — map for multi-entity caching
benchmarkDetailMap: Record<string, AgentEvalBenchmark>;
loadingBenchmarkDetailIds: string[]; // per-item loading
// Mutation states (drive form-level UI)
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.mdfor the full discriminated-union action types, theproduce-based reducer, and theinternal_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)
// 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
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 (exclude heavy fields)
- May include computed statistics for UI
- NOT
extendsDetail
- Use array for list data:
xxxList: XxxListItem[] - Use Map for detail data:
xxxDetailMap: Record<string, Xxx> - Per-item loading:
loadingXxxDetailIds: string[] - Reducer for detail map if optimistic updates needed (see
references/reducer.md) - Internal dispatch and loading methods
- Selectors for clean access (optional but recommended)
- Document in comments which fields are excluded from List and why
Best Practices
- File organization — one entity per file, not mixed
- List is a subset — ListItem excludes heavy fields, does not
extendsDetail - Clear naming —
xxxListfor arrays,xxxDetailMapfor 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:
// Wrong — pulls heavy fields back in
export interface BenchmarkListItem extends Benchmark {
testCaseCount?: number;
}
✅ DO create separate subset:
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-architecture— how to fetch and update this datazustand— general Zustand patterns
Signals
- GitHub stars
- 82k
- Forks
- 16k
- Last commit
- Sep 2026
ahel recommends instead
Advanced
- Catalog kind
- skill
- Gateway key
store-data-structures-lobehub- Source
- github.com/lobehub/lobehub