Source profileQuality 86/100

lobehub/lobehub/.agents/skills/data-fetching-architecture/SKILL.md

data-fetching-architecture

LobeHub data-fetching pipeline guide. Use for service layer, Zustand store, SWR, lambdaClient, useClientDataSWR, useFetchXxx hooks, or migrating useEffect fetches.

Source repository stars
81,246
Declared platforms
0
Static risk flags
0
Last source update
2026-08-04
Source checked
2026-08-04

Decision brief

What it does—and where it fits

Related: store-data-structures covers List vs Detail data shape rationale (Map vs Array).

Best for

    Not for

    • Tasks that require unconfirmed production actions or broad system permissions.
    • Environments where the pinned source and install steps cannot be inspected.

    Compatibility matrix

    Platform support, with evidence labels

    PlatformStatusEvidenceWhat to check
    CodexNot declaredNo explicit evidencePortability before use
    Claude CodeNot declaredNo explicit evidencePortability before use
    CursorNot declaredNo explicit evidencePortability before use
    Gemini CLINot declaredNo explicit evidencePortability before use
    Open the compatibility checker

    Installation

    Inspect first. Install second.

    The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

    Source-detected install commandSource
    npx skills add https://github.com/lobehub/lobehub --skill ".agents/skills/data-fetching-architecture"
    Safe inspection promptEditorial

    Inspect the Agent Skill "data-fetching-architecture" from https://github.com/lobehub/lobehub/blob/eb65d06d050732503da3be00f2902dc4483f53bd/.agents/skills/data-fetching-architecture/SKILL.md at commit eb65d06d050732503da3be00f2902dc4483f53bd. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

    Workflow

    What the source asks the agent to do

    1. 01

      Layer 3: Component Usage

      Why two patterns: create has no id yet, so a single isCreatingXxx flag is enough. Update/delete target a specific row, so global flags would freeze unrelated rows — keep per-item state in loadingXxxIds.

      Why two patterns: create has no id yet, so a single isCreatingXxx flag is enough. Update/delete target a specific row, so global flags would freeze unrelated rows — keep per-item state in loadingXxxIds.
    2. 02

      Step 1: Types & State

      See store-data-structures for details.

      [ ] Define types in @lobechat/types: Detail type + List item type[ ] State structure: xxxList: XxxListItem[], xxxDetailMap: Record, loadingXxxDetailIds: string[][ ] Reducer if optimistic updates are needed
    3. 03

      Step 2: Service Layer

      [ ] Create service in src/services/xxxService.ts

      [ ] Create service in src/services/xxxService.ts[ ] Methods: listXxx(), getXxx(id), createXxx(), updateXxx(), deleteXxx()- [ ] Create service in src/services/xxxService.ts - [ ] Methods: listXxx(), getXxx(id), createXxx(), updateXxx(), deleteXxx()
    4. 04

      Step 3: Store Actions

      [ ] initialState.ts with state structure

      [ ] initialState.ts with state structure[ ] action.ts with:[ ] useFetchXxxList(), useFetchXxxDetail(id) — SWR hooks
    5. 05

      Step 4: Component Usage

      Mental model: Types → Service → Reducer → Slice → Component 🎯

      [ ] Use store hooks (NOT useEffect)[ ] Destructure and consume error / mutate; wrap first-load surfaces in AsyncBoundary[ ] List pages: access xxxList array

    Permission review

    Static risk signals and limitations

    No configured static risk pattern was detected

    This is not proof of safety. Runtime behavior, indirect dependencies, and hidden external systems are outside the static scan.

    Evidence record

    Why each signal appears

    EvidenceSourceComputedTestedEditorial
    SignalValueEvidence typeMeaning
    Quality score86/100ComputedDocumentation, specificity, maintenance, and trust rules
    Repository stars81,246SourceRepository attention, not individual Skill quality
    Compatibility0 platformsSourceDeclared in the catalog source record
    Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

    Pinned source

    Provenance and original SKILL.md

    Repository
    lobehub/lobehub
    Skill path
    .agents/skills/data-fetching-architecture/SKILL.md
    Commit
    eb65d06d050732503da3be00f2902dc4483f53bd
    License
    NOASSERTION
    Collected
    2026-08-04
    Default branch
    canary
    View the original SKILL.md

    LobeHub Data Fetching Architecture

    Related: store-data-structures covers List vs Detail data shape rationale (Map vs Array).

    Architecture Overview

    ┌─────────────┐
    │  Component  │
    └──────┬──────┘
           │ 1. Call useFetchXxx hook from store
           ↓
    ┌──────────────────┐
    │  Zustand Store   │
    │  (State + Hook)  │
    └──────┬───────────┘
           │ 2. useClientDataSWR calls service
           ↓
    ┌──────────────────┐
    │  Service Layer   │
    │  (xxxService)    │
    └──────┬───────────┘
           │ 3. Call lambdaClient
           ↓
    ┌──────────────────┐
    │  lambdaClient    │
    │  (TRPC Client)   │
    └──────────────────┘
    

    Core Principles

    ✅ DO

    1. Use Service Layer for all API calls
    2. Use Store SWR Hooks for data fetching (not useEffect)
    3. Use proper data structures — see store-data-structures skill for List vs Detail patterns
    4. Use lambdaClient.mutate for write operations (create/update/delete)
    5. Use lambdaClient.query only inside service methods
    6. Naming convention — read hooks are useFetchXxx, cache invalidation helpers are refreshXxx (e.g. useFetchBenchmarks / refreshBenchmarks). Mutations then chain refreshXxx() after the service call.

    ❌ DON'T

    1. Never use useEffect for data fetching
    2. Never call lambdaClient directly in components or stores
    3. Never use useState for server data
    4. Never mix data structure patterns — follow store-data-structures skill

    Layer 1: Service Layer

    Purpose

    • Encapsulate all API calls to lambdaClient
    • Provide clean, typed interfaces
    • Single source of truth for API operations

    Service Structure

    // src/services/agentEval.ts
    class AgentEvalService {
      // Query methods - READ operations
      async listBenchmarks() {
        return lambdaClient.agentEval.listBenchmarks.query();
      }
    
      async getBenchmark(id: string) {
        return lambdaClient.agentEval.getBenchmark.query({ id });
      }
    
      // Mutation methods - WRITE operations
      async createBenchmark(params: CreateBenchmarkParams) {
        return lambdaClient.agentEval.createBenchmark.mutate(params);
      }
    
      async updateBenchmark(params: UpdateBenchmarkParams) {
        return lambdaClient.agentEval.updateBenchmark.mutate(params);
      }
    
      async deleteBenchmark(id: string) {
        return lambdaClient.agentEval.deleteBenchmark.mutate({ id });
      }
    }
    
    export const agentEvalService = new AgentEvalService();
    

    Service Guidelines

    1. One service per domain (e.g., agentEval, ragEval, aiAgent)
    2. Export singleton instance (export const xxxService = new XxxService())
    3. Method names match operations (list, get, create, update, delete)
    4. Clear parameter types (use interfaces for complex params)

    Layer 2: Store with SWR Hooks

    Purpose

    • Manage client-side state
    • Provide SWR hooks for data fetching
    • Handle cache invalidation

    State Structure

    // src/store/eval/slices/benchmark/initialState.ts
    export interface BenchmarkSliceState {
      // List data - simple array
      benchmarkList: AgentEvalBenchmarkListItem[];
      benchmarkListInit: boolean;
    
      // Detail data - map for caching
      benchmarkDetailMap: Record<string, AgentEvalBenchmark>;
      loadingBenchmarkDetailIds: string[];
    
      // Mutation states
      isCreatingBenchmark: boolean;
      isUpdatingBenchmark: boolean;
      isDeletingBenchmark: boolean;
    }
    

    For complete initialState, reducer, and internal dispatch patterns, see the store-data-structures skill.

    Actions

    // src/store/eval/slices/benchmark/action.ts
    const FETCH_BENCHMARKS_KEY = 'FETCH_BENCHMARKS';
    const FETCH_BENCHMARK_DETAIL_KEY = 'FETCH_BENCHMARK_DETAIL';
    
    export interface BenchmarkAction {
      // SWR Hooks - for data fetching
      useFetchBenchmarks: () => SWRResponse;
      useFetchBenchmarkDetail: (id?: string) => SWRResponse;
    
      // Refresh methods - for cache invalidation
      refreshBenchmarks: () => Promise<void>;
      refreshBenchmarkDetail: (id: string) => Promise<void>;
    
      // Mutation actions
      createBenchmark: (params: CreateParams) => Promise<any>;
      updateBenchmark: (params: UpdateParams) => Promise<void>;
      deleteBenchmark: (id: string) => Promise<void>;
    
      // Internal methods - not for direct UI use
      internal_dispatchBenchmarkDetail: (payload: BenchmarkDetailDispatch) => void;
      internal_updateBenchmarkDetailLoading: (id: string, loading: boolean) => void;
    }
    
    export const createBenchmarkSlice: StateCreator<EvalStore, any, [], BenchmarkAction> = (
      set,
      get,
    ) => ({
      // Fetch list — simple array stored in benchmarkList
      useFetchBenchmarks: () =>
        useClientDataSWR(FETCH_BENCHMARKS_KEY, () => agentEvalService.listBenchmarks(), {
          onSuccess: (data) => {
            set({ benchmarkList: data, benchmarkListInit: true }, false, 'useFetchBenchmarks/success');
          },
        }),
    
      // Fetch detail — null key disables the request when id is missing
      useFetchBenchmarkDetail: (id) =>
        useClientDataSWR(
          id ? [FETCH_BENCHMARK_DETAIL_KEY, id] : null,
          () => agentEvalService.getBenchmark(id!),
          {
            onSuccess: (data) => {
              get().internal_dispatchBenchmarkDetail({
                type: 'setBenchmarkDetail',
                id: id!,
                value: data,
              });
              get().internal_updateBenchmarkDetailLoading(id!, false);
            },
          },
        ),
    
      // Refresh methods
      refreshBenchmarks: () => mutate(FETCH_BENCHMARKS_KEY),
      refreshBenchmarkDetail: (id) => mutate([FETCH_BENCHMARK_DETAIL_KEY, id]),
    
      // CREATE — refresh list after creation
      createBenchmark: async (params) => {
        set({ isCreatingBenchmark: true }, false, 'createBenchmark/start');
        try {
          const result = await agentEvalService.createBenchmark(params);
          await get().refreshBenchmarks();
          return result;
        } finally {
          set({ isCreatingBenchmark: false }, false, 'createBenchmark/end');
        }
      },
    
      // UPDATE — optimistic update + refresh
      updateBenchmark: async (params) => {
        const { id } = params;
    
        // 1. Optimistic update
        get().internal_dispatchBenchmarkDetail({
          type: 'updateBenchmarkDetail',
          id,
          value: params,
        });
        // 2. Set loading
        get().internal_updateBenchmarkDetailLoading(id, true);
    
        try {
          // 3. Call service
          await agentEvalService.updateBenchmark(params);
          // 4. Refresh from server
          await get().refreshBenchmarks();
          await get().refreshBenchmarkDetail(id);
        } finally {
          get().internal_updateBenchmarkDetailLoading(id, false);
        }
      },
    
      // DELETE — optimistic update + refresh
      deleteBenchmark: async (id) => {
        get().internal_dispatchBenchmarkDetail({ type: 'deleteBenchmarkDetail', id });
        get().internal_updateBenchmarkDetailLoading(id, true);
    
        try {
          await agentEvalService.deleteBenchmark(id);
          await get().refreshBenchmarks();
        } finally {
          get().internal_updateBenchmarkDetailLoading(id, false);
        }
      },
    
      // Internal — dispatch to reducer (for detail map)
      internal_dispatchBenchmarkDetail: (payload) => {
        const currentMap = get().benchmarkDetailMap;
        const nextMap = benchmarkDetailReducer(currentMap, payload);
    
        // Skip set when nothing changed — avoids unnecessary re-renders
        if (isEqual(nextMap, currentMap)) return;
        set({ benchmarkDetailMap: nextMap }, false, `dispatchBenchmarkDetail/${payload.type}`);
      },
    
      // Internal — update loading state for specific detail
      internal_updateBenchmarkDetailLoading: (id, loading) => {
        set(
          (state) => ({
            loadingBenchmarkDetailIds: loading
              ? [...state.loadingBenchmarkDetailIds, id]
              : state.loadingBenchmarkDetailIds.filter((i) => i !== id),
          }),
          false,
          'updateBenchmarkDetailLoading',
        );
      },
    });
    

    Store Guidelines

    1. SWR keys as constants at top of file
    2. useClientDataSWR for all data fetching (never useEffect)
    3. onSuccess/onData callback updates store state
    4. Refresh methods use mutate() to invalidate cache
    5. Loading states in initialState, updated in onSuccess/onData
    6. Mutations call service, then refresh relevant cache

    Async Failure Boundary Contract

    Every read hook returns an SWR response that includes error and mutate; the surface must consume them. A success-only init flag (!isInit, !map[id], data ?? []) is not enough: when the request fails, that flag often never flips, so the UI paints a permanent skeleton, a fake empty state, a false NotFound, or a confident zero-value metric.

    Use the shared UI primitives:

    • AsyncBoundary for normal loading / error / empty / data surfaces.
    • AsyncError for custom layouts, detail pages, inline load-more failures, or metrics.

    Core precedence for first-load failures:

    const { data, error, isLoading, mutate } = useFetchXxx();
    
    return (
      <AsyncBoundary
        data={data}
        empty={<EmptyState />}
        error={error}
        isEmpty={!error && data?.length === 0}
        isLoading={isLoading}
        onRetry={() => {
          void mutate();
        }}
      >
        <List items={data ?? []} />
      </AsyncBoundary>
    );
    

    Rules:

    • Check error before empty / NotFound / zero defaults. Error is not a kind of empty.
    • Keep already-loaded content on background revalidation failures; only replace the surface when there is no settled data to preserve.
    • For detail maps, don't put the error branch after if (!map[id]) return <Skeleton/>. First-load failures never populate the map, making that error branch unreachable.
    • For infinite scroll / load-more, persist a per-bucket loadMoreError and render an inline Retry row. Do not let an IntersectionObserver silently retry while the error is still unresolved.
    • For merged fetched + static lists, branch on the fetched slice's error before merging. Static fallback rows can make a failed fetch look like a plausible partial catalog.

    Layer 3: Component Usage

    Fetching List Data

    // ✅ CORRECT
    const BenchmarkList = () => {
      // 1. Get the hook from store
      const useFetchBenchmarks = useEvalStore((s) => s.useFetchBenchmarks);
    
      // 2. Get list data
      const benchmarks = useEvalStore((s) => s.benchmarkList);
      const isInit = useEvalStore((s) => s.benchmarkListInit);
    
      // 3. Call the hook (SWR handles the data fetching)
      useFetchBenchmarks();
    
      // 4. Use the data
      if (!isInit) return <Loading />;
      return (
        <div>
          <h2>Total: {benchmarks.length}</h2>
          {benchmarks.map((b) => (
            <BenchmarkCard key={b.id} {...b} />
          ))}
        </div>
      );
    };
    

    Fetching Detail Data

    // ✅ CORRECT
    const BenchmarkDetail = () => {
      const { benchmarkId } = useParams<{ benchmarkId: string }>();
    
      const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail);
    
      // Detail from map
      const benchmark = useEvalStore((s) =>
        benchmarkId ? s.benchmarkDetailMap[benchmarkId] : undefined,
      );
    
      // Per-item loading
      const isLoading = useEvalStore((s) =>
        benchmarkId ? s.loadingBenchmarkDetailIds.includes(benchmarkId) : false,
      );
    
      useFetchBenchmarkDetail(benchmarkId);
    
      if (!benchmark) return <Loading />;
      return (
        <div>
          <h1>{benchmark.name}</h1>
          <p>{benchmark.description}</p>
          {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),
    };
    
    // Component with selectors
    const BenchmarkDetail = () => {
      const { benchmarkId } = useParams();
      const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail);
      const benchmark = useEvalStore(benchmarkSelectors.getBenchmarkDetail(benchmarkId!));
    
      useFetchBenchmarkDetail(benchmarkId);
    
      return <div>{benchmark && <h1>{benchmark.name}</h1>}</div>;
    };
    

    Anti-pattern

    // ❌ WRONG — Don't use useEffect for data fetching
    const BenchmarkList = () => {
      const [data, setData] = useState([]);
      const [loading, setLoading] = useState(false);
    
      useEffect(() => {
        setLoading(true);
        lambdaClient.agentEval.listBenchmarks
          .query()
          .then(setData)
          .finally(() => setLoading(false));
      }, []);
    
      return <div>...</div>;
    };
    

    Mutations in Components

    // Create — global mutation flag drives form loading
    const CreateBenchmarkModal = () => {
      const createBenchmark = useEvalStore((s) => s.createBenchmark);
      const isCreating = useEvalStore((s) => s.isCreatingBenchmark);
    
      const handleSubmit = async (values) => {
        try {
          // Optimistic update + refresh happen inside createBenchmark
          await createBenchmark(values);
          message.success('Created successfully');
          onClose();
        } catch (error) {
          message.error('Failed to create');
        }
      };
    
      return (
        <Form onSubmit={handleSubmit} loading={isCreating}>
          ...
        </Form>
      );
    };
    
    // Update / delete — per-item loading so only the row being mutated spins
    const BenchmarkItem = ({ id }: { id: string }) => {
      const updateBenchmark = useEvalStore((s) => s.updateBenchmark);
      const deleteBenchmark = useEvalStore((s) => s.deleteBenchmark);
      const isLoading = useEvalStore(benchmarkSelectors.isLoadingBenchmarkDetail(id));
    
      const handleUpdate = async (data) => {
        await updateBenchmark({ id, ...data });
      };
    
      const handleDelete = async () => {
        await deleteBenchmark(id);
      };
    
      return (
        <div>
          {isLoading && <Spinner />}
          <button onClick={handleUpdate}>Update</button>
          <button onClick={handleDelete}>Delete</button>
        </div>
      );
    };
    

    Why two patterns: create has no id yet, so a single isCreatingXxx flag is enough. Update/delete target a specific row, so global flags would freeze unrelated rows — keep per-item state in loadingXxxIds.


    Need a fuller worked example?

    The canonical Benchmark example above is the one to copy for a flat list + detail map. If you need to maintain a list keyed by a parent id (e.g. datasetMap[benchmarkId] because the same shape appears under multiple parents), read references/walkthrough.md — it walks through the full 6 steps (service → reducer → slice → store wiring → selectors → component) for that variant.


    Common Patterns

    Pattern 1: Pagination

    Cache key array must include every parameter that should trigger a refetch.

    useFetchTestCases: (params: { datasetId: string; limit: number; offset: number }) =>
      useClientDataSWR(
        params.datasetId ? [FETCH_TEST_CASES_KEY, params.datasetId, params.limit, params.offset] : null,
        () => agentEvalService.listTestCases(params),
        {
          onSuccess: (data) =>
            set({
              testCaseList: data.data,
              testCaseTotal: data.total,
              isLoadingTestCases: false,
            }),
        },
      );
    

    Pattern 2: Dependent Fetching

    Both hooks run in parallel — SWR dedupes, no manual sequencing needed.

    const BenchmarkDetail = () => {
      const { benchmarkId } = useParams();
      const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail);
      const useFetchDatasets = useEvalStore((s) => s.useFetchDatasets);
    
      useFetchBenchmarkDetail(benchmarkId);
      useFetchDatasets(benchmarkId);
    
      return <div>...</div>;
    };
    

    Pattern 3: Conditional Fetching

    Pass undefined to disable the hook entirely.

    // only fetch when modal is open AND id present
    useFetchDatasetDetail(open && datasetId ? datasetId : undefined);
    

    Pattern 4: Cross-domain Refresh

    deleteBenchmark: async (id) => {
      await agentEvalService.deleteBenchmark(id);
      await get().refreshBenchmarks();
      await get().refreshDatasets(id); // related cache invalidated too
    };
    

    Migration Guide: useEffect → Store SWR

    Before (❌ Wrong)

    const TestCaseList = ({ datasetId }: Props) => {
      const [data, setData] = useState<any[]>([]);
      const [loading, setLoading] = useState(false);
    
      useEffect(() => {
        setLoading(true);
        lambdaClient.agentEval.listTestCases
          .query({ datasetId })
          .then((r) => setData(r.data))
          .finally(() => setLoading(false));
      }, [datasetId]);
    
      return <Table data={data} loading={loading} />;
    };
    

    After (✅ Correct)

    // 1. Add service method
    class AgentEvalService {
      async listTestCases(params: { datasetId: string }) {
        return lambdaClient.agentEval.listTestCases.query(params);
      }
    }
    
    // 2. Add store slice hook
    export const createTestCaseSlice: StateCreator<...> = (set) => ({
      useFetchTestCases: (params) =>
        useClientDataSWR(
          params.datasetId ? [FETCH_TEST_CASES_KEY, params.datasetId] : null,
          () => agentEvalService.listTestCases(params),
          {
            onSuccess: (data) =>
              set({ testCaseList: data.data, isLoadingTestCases: false }),
          },
        ),
    });
    
    // 3. Component reads from store
    const TestCaseList = ({ datasetId }: Props) => {
      const useFetchTestCases = useEvalStore((s) => s.useFetchTestCases);
      const data = useEvalStore((s) => s.testCaseList);
      const loading = useEvalStore((s) => s.isLoadingTestCases);
    
      useFetchTestCases({ datasetId });
    
      return <Table data={data} loading={loading} />;
    };
    

    Troubleshooting

    SymptomCheck
    Data never loadsHook called? Key not null/undefined? Network tab shows request?
    Stale data after mutationDid refreshXxx run? Cache key matches what the hook uses?
    Loading state stuck trueonSuccess writes loading=false? Promise rejected silently?
    Detail map missing an entryReducer dispatch ran? isEqual short-circuited on stale data?

    Summary Checklist

    When adding new data fetching:

    Step 1: Types & State

    See store-data-structures for details.

    • Define types in @lobechat/types: Detail type + List item type
    • State structure: xxxList: XxxListItem[], xxxDetailMap: Record<string, Xxx>, loadingXxxDetailIds: string[]
    • Reducer if optimistic updates are needed

    Step 2: Service Layer

    • Create service in src/services/xxxService.ts
    • Methods: listXxx(), getXxx(id), createXxx(), updateXxx(), deleteXxx()

    Step 3: Store Actions

    • initialState.ts with state structure
    • action.ts with:
      • useFetchXxxList(), useFetchXxxDetail(id) — SWR hooks
      • refreshXxxList(), refreshXxxDetail(id) — cache invalidation
      • CRUD methods calling service
      • internal_dispatch, internal_updateLoading if using reducer
    • selectors.ts (optional but recommended)
    • Integrate slice into main store + initialState

    Step 4: Component Usage

    • Use store hooks (NOT useEffect)
    • Destructure and consume error / mutate; wrap first-load surfaces in AsyncBoundary or render AsyncError before empty / NotFound / zero defaults
    • List pages: access xxxList array
    • Detail pages: access xxxDetailMap[id]
    • Use loading states for UI feedback
    • Infinite-scroll failures persist as a visible tail Retry row, not a silent catch

    Mental model: Types → Service → Reducer → Slice → Component 🎯


    Related Skills

    • store-data-structures — How to structure List and Detail data in stores
    • zustand — General Zustand patterns and best practices