Skip to main content
ClaudeWave
Skill78.6k repo starsupdated today

store-data-structures

# Claude Code Skill Definition: store-data-structures This Claude Code skill provides architectural patterns for organizing Zustand store state in LobeHub projects, specifically for separating list and detail data, using normalized maps for cached details, and applying reducer patterns for optimistic updates. Use when designing store structures to avoid performance pitfalls like single-object caching, mixing list and detail types, or using heavy database types, ensuring fast list rendering and ergonomic state mutations.

Install in Claude Code
Copy
git clone --depth 1 https://github.com/lobehub/lobehub /tmp/store-data-structures && cp -r /tmp/store-data-structures/.agents/skills/store-data-structures ~/.claude/skills/store-data-structures
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# 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`](./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

```typescript
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

```typescript
benchmarkList: AgentEvalBenchmarkListItem[];
```

Examples: benchmark list, dataset list, user list.

---

## State Structure Pattern

```typescript
// 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`](./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

```typescript
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

```typescript
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

```tsx
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

```tsx
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)

```typescript
// 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(benchmarkS