New to Claude Skills? Learn how to install them →

lobehub on GitHub

LobeHub Store Data Structures

Free

Optimize Zustand store data for efficient rendering.

by lobehub81.5k stars on lobehub/lobehub
3 views
Updated Aug 1, 2026
Get this skill

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 source

1. Install with the skills CLI

npx skills add lobehub/lobehub/store-data-structures --agent claude-code

2. 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 lobehub

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

  1. Separate List and Detail — different structures for list pages and detail pages
  2. Use Map for Details — cache multiple detail pages with Record<string, Detail>
  3. Use Array for Lists — simple arrays for list display
  4. Types from @lobechat/types — never use @lobechat/database types in stores
  5. Distinguish List and Detail types — List types may have computed UI fields

❌ DON'T

  1. Don't use a single detail object — can't cache multiple pages
  2. Don't mix List and Detail types — they have different purposes
  3. Don't use database types — use types from @lobechat/types
  4. 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

// 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.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)

// 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 extends Detail
  • 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

  1. File organization — one entity per file, not mixed
  2. List is a subset — ListItem excludes heavy fields, does not extends Detail
  3. Clear namingxxxList for arrays, xxxDetailMap for maps
  4. Consistent patterns — all detail maps follow the same shape
  5. Type safety — never use any, always use proper types
  6. Document exclusions — comment which fields are excluded and why
  7. Selectors — encapsulate access patterns
  8. Loading states — per-item for details, global for mutations
  9. 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 data
  • zustand — general Zustand patterns

Frequently asked questions about LobeHub Store Data Structures

Similar skills