.agents/skills/store-data-structures/SKILL.md
How to structure data in Zustand stores for fast list rendering, multi-detail caching, and ergonomic optimistic updates.
Record<string, Detail>@lobechat/types — never use @lobechat/database types in stores@lobechat/typesEach entity gets its own file under @lobechat/types/. Each file exports two types:
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.
✅ 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.
✅ 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.
// 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,
};
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.
interface BenchmarkSliceState {
benchmarkDetail: AgentEvalBenchmark | null;
isLoadingBenchmarkDetail: boolean;
}
Problems:
interface BenchmarkSliceState {
benchmarkList: AgentEvalBenchmarkListItem[];
benchmarkListInit: boolean;
benchmarkDetailMap: Record<string, AgentEvalBenchmark>;
loadingBenchmarkDetailIds: string[];
isCreatingBenchmark: boolean;
isUpdatingBenchmark: boolean;
isDeletingBenchmark: boolean;
}
Benefits:
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>
);
};
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>
);
};
// 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!));
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
When designing store state structure:
benchmark.ts, agentEvalDataset.ts)extends DetailxxxList: XxxListItem[]xxxDetailMap: Record<string, Xxx>loadingXxxDetailIds: string[]references/reducer.md)extends DetailxxxList for arrays, xxxDetailMap for mapsany, always use proper types❌ 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
data-fetching — how to fetch and update this datazustand — general Zustand patterns