
LobeHub Store Data Structures
FreeOptimize Zustand store data for efficient rendering.
Free · Opens the source repo
What LobeHub Store Data Structures does
LobeHub Store Data Structures provides guidance on structuring data within Zustand stores to enhance performance and usability in applications. It emphasizes the separation of list and detail data, recommending the use of simple arrays for lists and maps for detail pages. This approach allows for efficient caching of multiple detail pages, enabling users to navigate between them without the need for refetching data. The skill is particularly beneficial for developers looking to implement optimistic updates, where the UI reflects changes before server confirmation, thus improving user experience.
The skill outlines core principles that developers should adhere to when designing their store state. It advises against using a single detail object, which can hinder caching and lead to performance bottlenecks. Instead, it promotes the use of distinct types for lists and details, ensuring that the data structure is optimized for the specific use case. By following these guidelines, developers can create a more responsive and efficient application, especially in scenarios where quick data access and updates are crucial.
Additionally, the skill includes detailed type definitions that help maintain clarity and separation between list items and detail entities. Each entity is defined in its own file, ensuring that the heavy fields of detail types do not inadvertently affect list performance. This structured approach facilitates better maintainability and scalability of the application, making it easier for teams to collaborate and extend functionality without introducing errors.
Overall, LobeHub Store Data Structures serves as a practical resource for developers and designers working with Zustand stores, providing them with the necessary patterns and best practices to create high-performance applications that handle data efficiently.
When to use it
Use this skill when designing Zustand store states, particularly for applications requiring fast list rendering and efficient detail page management.
When not to use it
This skill may not be suitable for applications that do not utilize Zustand or for those that do not require complex data structures.
What you can build with it
Optimizing List Rendering
Implementing the recommended array structure for list data allows for quick and efficient rendering of items in a user interface.
Caching Multiple Detail Pages
Using a map for detail data enables users to switch between multiple detail views without the need for repeated data fetching.
Implementing Optimistic Updates
By structuring the state with reducers, developers can reflect changes in the UI immediately, enhancing user experience during data updates.
How to install LobeHub Store Data Structures
View source1. Install with the skills CLI
npx skills add lobehub/lobehub/store-data-structures --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by lobehubLobeHub 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/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
Frequently asked questions about LobeHub Store Data Structures
Similar skills
Spring Boot Testing
Master testing techniques for Spring Boot 4 applications.
GitHub Issues
Manage GitHub issues efficiently with MCP tools.
Geofeed Tuner
Optimize your IP geolocation feeds in CSV format.
Batch Files
Master Windows batch scripting for automation and task management.
Adobe Illustrator Scripting
Automate your Illustrator workflows with ExtendScript.
Plugin Structure
Create and organize Claude Code plugins effectively.
