with one click
store-data-structures
LobeHub Zustand store data-shape patterns. Use when designing store state, list/detail splits, normalized maps, reducers, messagesMap, topicsMap, or choosing shared type sources.
Menu
LobeHub Zustand store data-shape patterns. Use when designing store state, list/detail splits, normalized maps, reducers, messagesMap, topicsMap, or choosing shared type sources.
| 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 |
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-architecture — how to fetch and update this datazustand — general Zustand patternsAgentic end-to-end testing for LobeHub: backend verification via the CLI, frontend verification via agent-browser (Electron), full-stack verification in the browser, and bot-channel verification via osascript. Local-first today, designed to extend to cloud automation. Triggers on 'cli test', 'test with cli', 'verify with cli', 'backend test with cli', 'local test', 'test in electron', 'test desktop', 'test bot', 'bot test', 'test in discord', 'test in telegram', 'test in slack', 'test in wechat', 'test in weixin', 'test in lark', 'test in feishu', 'test in qq', 'manual test', 'osascript', 'test report', or any local end-to-end verification task.
LobeHub product design values / principles / checklists. Load this skill whenever the work touches user-interface features or implementation — designing or building any user-facing flow — to get better UX results.
LobeHub React component conventions. Use when editing TSX UI, choosing base-ui vs @lobehub/ui vs antd, styling with antd-style, routing, desktop variants, layouts, or component state.
Vitest testing guide. Use when writing or updating tests, fixing failing tests, improving coverage, debugging test issues, or setting up mocks.
Version release workflow — release process and GitHub Release notes (not docs/changelog pages).
Audit .agents/skills SKILL.md files. Use for recurring checks of duplicate, overlapping, stale, inconsistent, or broken skills and merge/delete candidates.