Token导航 LogoToken导航TokenDH.com
前端设计需要联网github未标认证来源可访问许可证需确认审计通过

data-fetching数据获取

Agent Skill

用于辅助数据整理、表格处理、CSV/Excel 分析、指标计算和图表准备。它适合让 Agent 清洗字段、汇总数据、发现异常、生成统计口径或把分析结果转成可读说明。使用时需要确认数据来源、字段含义和时间范围,避免把样本数据当全量事实;涉及敏感数据、导出文件或批量写回时,应先确认权限和脱敏边界。

总安装

12,799

周安装

528

GitHub Stars

75,921

下载量

4,182
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

unknown

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:data-fetching(数据获取)
来源仓库:https://github.com/lobehub/lobehub
仓库路径:skills/data-fetching
安装命令:
npx skills add https://github.com/lobehub/lobehub --skill data-fetching
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/lobehub/lobehub --skill data-fetching

简介

用于辅助数据整理、表格处理和指标计算。data-fetching 属于前端设计类 Skill,可作为该场景下的辅助能力补充。

  • 适合清洗字段、汇总数据、发现异常或生成统计口径。
  • 需确认数据来源、字段含义和时间范围,避免把样本当全量。
  • 安装方式:通过 npx skills add 命令从 GitHub 仓库添加。
  • 涉及敏感数据或批量写回应先确认脱敏边界和权限。

SKILL.md

LobeHub Data Fetching Architecture

Related Skills: - store-data-structures - How to structure List and Detail data in stores (Map vs Array patterns)

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

❌ 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
Note: For data structure patterns (Map vs Array, List vs Detail), see the 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
import { lambdaClient } from '@/libs/trpc/client';

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
Data Structure: See store-data-structures skill for how to structure List and Detail data.

Store Structure Overview

// src/store/eval/slices/benchmark/initialState.ts
import type { AgentEvalBenchmark, AgentEvalBenchmarkListItem } from '@lobechat/types';

export interface BenchmarkSliceState {
  // List data - simple array (see store-data-structures skill)
  benchmarkList: AgentEvalBenchmarkListItem[];
  benchmarkListInit: boolean;

  // Detail data - map for caching (see store-data-structures skill)
  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.

Create Actions

// src/store/eval/slices/benchmark/action.ts
import type { SWRResponse } from 'swr';
import type { StateCreator } from 'zustand/vanilla';
import isEqual from 'fast-deep-equal';

import { mutate, useClientDataSWR } from '@/libs/swr';
import { agentEvalService } from '@/services/agentEval';
import type { EvalStore } from '@/store/eval/store';
import { benchmarkDetailReducer, type BenchmarkDetailDispatch } from './reducer';

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 - for write operations
  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,
  [['zustand/devtools', never]],
  [],
  BenchmarkAction
> = (set, get) => ({
  // Fetch list - Simple array
  useFetchBenchmarks: () => {
    return useClientDataSWR(FETCH_BENCHMARKS_KEY, () => agentEvalService.listBenchmarks(), {
      onSuccess: (data: any) => {
        set(
          {
            benchmarkList: data,
            benchmarkListInit: true,
          },
          false,
          'useFetchBenchmarks/success',
        );
      },
    });
  },

  // Fetch detail - Map with dispatch
  useFetchBenchmarkDetail: (id) => {
    return useClientDataSWR(
      id ? [FETCH_BENCHMARK_DETAIL_KEY, id] : null,
      () => agentEvalService.getBenchmark(id!),
      {
        onSuccess: (data: any) => {
          get().internal_dispatchBenchmarkDetail({
            type: 'setBenchmarkDetail',
            id: id!,
            value: data,
          });
          get().internal_updateBenchmarkDetailLoading(id!, false);
        },
      },
    );
  },

  // Refresh methods
  refreshBenchmarks: async () => {
    await mutate(FETCH_BENCHMARKS_KEY);
  },

  refreshBenchmarkDetail: async (id) => {
    await 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 - With optimistic update for detail
  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 - Refresh list and remove from detail map
  deleteBenchmark: async (id) => {
    // 1. Optimistic update
    get().internal_dispatchBenchmarkDetail({
      type: 'deleteBenchmarkDetail',
      id,
    });

    // 2. Set loading
    get().internal_updateBenchmarkDetailLoading(id, true);

    try {
      // 3. Call service
      await agentEvalService.deleteBenchmark(id);

      // 4. Refresh list
      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);

    // No need to update if map is the same
    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) => {
        if (loading) {
          return { loadingBenchmarkDetailIds: [...state.loadingBenchmarkDetailIds, id] };
        }
        return {
          loadingBenchmarkDetailIds: 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 callback updates store state
  4. Refresh methods use mutate() to invalidate cache
  5. Loading states in initialState, updated in onSuccess
  6. Mutations call service, then refresh relevant cache

Layer 3: Component Usage

Data Fetching in Components

Fetching List Data:

// Component using list data - ✅ CORRECT
import { useEvalStore } from '@/store/eval';

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:

// Component using detail data from map - ✅ CORRECT
import { useEvalStore } from '@/store/eval';
import { useParams } from 'react-router-dom';

const BenchmarkDetail = () => {
  const { benchmarkId } = useParams<{ benchmarkId: string }>();

  // 1. Get the hook
  const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail);

  // 2. Get detail from map
  const benchmark = useEvalStore((s) =>
    benchmarkId ? s.benchmarkDetailMap[benchmarkId] : undefined,
  );

  // 3. Get loading state
  const isLoading = useEvalStore((s) =>
    benchmarkId ? s.loadingBenchmarkDetailIds.includes(benchmarkId) : false,
  );

  // 4. Call the hook
  useFetchBenchmarkDetail(benchmarkId);

  // 5. Use the data
  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>;
};

What NOT to Do

// ❌ WRONG - Don't use useEffect for data fetching
const BenchmarkList = () => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      const result = await lambdaClient.agentEval.listBenchmarks.query();
      setData(result);
      setLoading(false);
    };
    fetchData();
  }, []);

  return <div>...</div>;
};

Mutations in Components

// Mutations (Create/Update/Delete) with optimistic updates - ✅ CORRECT
import { useEvalStore } from '@/store/eval';
import { benchmarkSelectors } from '@/store/eval/selectors';

const CreateBenchmarkModal = () => {
  const createBenchmark = useEvalStore((s) => s.createBenchmark);

  const handleSubmit = async (values) => {
    try {
      // Optimistic update happens inside createBenchmark
      await createBenchmark(values);
      message.success('Created successfully');
      onClose();
    } catch (error) {
      message.error('Failed to create');
    }
  };

  return <Form onSubmit={handleSubmit}>...</Form>;
};

// With loading state for specific item
const BenchmarkItem = ({ id }: { id: string }) => {
  const updateBenchmark = useEvalStore((s) => s.updateBenchmark);
  const deleteBenchmark = useEvalStore((s) => s.deleteBenchmark);
  const isLoading = useEvalStore(benchmarkSelectors.isLoadingBenchmark(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>
  );
};

Data Structures: For detailed comparison of List vs Detail patterns, see the store-data-structures skill.

Complete Example: Adding a New Feature

Scenario: Add "Dataset" data fetching with optimistic updates

Step 1: Create Service

// src/services/agentEval.ts
class AgentEvalService {
  // ... existing methods ...

  // Add new methods
  async listDatasets(benchmarkId: string) {
    return lambdaClient.agentEval.listDatasets.query({ benchmarkId });
  }

  async getDataset(id: string) {
    return lambdaClient.agentEval.getDataset.query({ id });
  }

  async createDataset(params: CreateDatasetParams) {
    return lambdaClient.agentEval.createDataset.mutate(params);
  }
}

Step 2: Create Reducer

// src/store/eval/slices/dataset/reducer.ts
import { produce } from 'immer';
import type { Dataset } from '@/types/dataset';

type AddDatasetAction = {
  type: 'addDataset';
  value: Dataset;
};

type UpdateDatasetAction = {
  id: string;
  type: 'updateDataset';
  value: Partial<Dataset>;
};

type DeleteDatasetAction = {
  id: string;
  type: 'deleteDataset';
};

export type DatasetDispatch = AddDatasetAction | UpdateDatasetAction | DeleteDatasetAction;

export const datasetReducer = (state: Dataset[] = [], payload: DatasetDispatch): Dataset[] => {
  switch (payload.type) {
    case 'addDataset': {
      return produce(state, (draft) => {
        draft.unshift(payload.value);
      });
    }

    case 'updateDataset': {
      return produce(state, (draft) => {
        const index = draft.findIndex((item) => item.id === payload.id);
        if (index !== -1) {
          draft[index] = { ...draft[index], ...payload.value };
        }
      });
    }

    case 'deleteDataset': {
      return produce(state, (draft) => {
        const index = draft.findIndex((item) => item.id === payload.id);
        if (index !== -1) {
          draft.splice(index, 1);
        }
      });
    }

    default:
      return state;
  }
};

Step 3: Create Store Slice

// src/store/eval/slices/dataset/initialState.ts
import type { Dataset } from '@/types/dataset';

export interface DatasetData {
  currentPage: number;
  hasMore: boolean;
  isLoading: boolean;
  items: Dataset[];
  pageSize: number;
  total: number;
}

export interface DatasetSliceState {
  // Map keyed by benchmarkId
  datasetMap: Record<string, DatasetData>;
  // Simple state for single item (read-only, used in modals)
  datasetDetail: Dataset | null;
  isLoadingDatasetDetail: boolean;
  loadingDatasetIds: string[];
}

export const datasetInitialState: DatasetSliceState = {
  datasetMap: {},
  datasetDetail: null,
  isLoadingDatasetDetail: false,
  loadingDatasetIds: [],
};
// src/store/eval/slices/dataset/action.ts
import type { SWRResponse } from 'swr';
import type { StateCreator } from 'zustand/vanilla';
import isEqual from 'fast-deep-equal';

import { mutate, useClientDataSWR } from '@/libs/swr';
import { agentEvalService } from '@/services/agentEval';
import type { EvalStore } from '@/store/eval/store';
import { datasetReducer, type DatasetDispatch } from './reducer';

const FETCH_DATASETS_KEY = 'FETCH_DATASETS';
const FETCH_DATASET_DETAIL_KEY = 'FETCH_DATASET_DETAIL';

export interface DatasetAction {
  // SWR Hooks
  useFetchDatasets: (benchmarkId?: string) => SWRResponse;
  useFetchDatasetDetail: (id?: string) => SWRResponse;

  // Refresh methods
  refreshDatasets: (benchmarkId: string) => Promise<void>;
  refreshDatasetDetail: (id: string) => Promise<void>;

  // Mutations
  createDataset: (params: any) => Promise<any>;
  updateDataset: (params: any) => Promise<void>;
  deleteDataset: (id: string, benchmarkId: string) => Promise<void>;

  // Internal methods
  internal_dispatchDataset: (payload: DatasetDispatch, benchmarkId: string) => void;
  internal_updateDatasetLoading: (id: string, loading: boolean) => void;
}

export const createDatasetSlice: StateCreator<
  EvalStore,
  [['zustand/devtools', never]],
  [],
  DatasetAction
> = (set, get) => ({
  // Fetch list with Map
  useFetchDatasets: (benchmarkId) => {
    return useClientDataSWR(
      benchmarkId ? [FETCH_DATASETS_KEY, benchmarkId] : null,
      () => agentEvalService.listDatasets(benchmarkId!),
      {
        onSuccess: (data: any) => {
          set(
            {
              datasetMap: {
                ...get().datasetMap,
                [benchmarkId!]: {
                  currentPage: 1,
                  hasMore: false,
                  isLoading: false,
                  items: data,
                  pageSize: data.length,
                  total: data.length,
                },
              },
            },
            false,
            'useFetchDatasets/success',
          );
        },
      },
    );
  },

  // Fetch single item (for modal display)
  useFetchDatasetDetail: (id) => {
    return useClientDataSWR(
      id ? [FETCH_DATASET_DETAIL_KEY, id] : null,
      () => agentEvalService.getDataset(id!),
      {
        onSuccess: (data: any) => {
          set(
            { datasetDetail: data, isLoadingDatasetDetail: false },
            false,
            'useFetchDatasetDetail/success',
          );
        },
      },
    );
  },

  refreshDatasets: async (benchmarkId) => {
    await mutate([FETCH_DATASETS_KEY, benchmarkId]);
  },

  refreshDatasetDetail: async (id) => {
    await mutate([FETCH_DATASET_DETAIL_KEY, id]);
  },

  // CREATE with optimistic update
  createDataset: async (params) => {
    const tmpId = Date.now().toString();
    const { benchmarkId } = params;

    get().internal_dispatchDataset(
      {
        type: 'addDataset',
        value: { ...params, id: tmpId, createdAt: Date.now() } as any,
      },
      benchmarkId,
    );

    get().internal_updateDatasetLoading(tmpId, true);

    try {
      const result = await agentEvalService.createDataset(params);
      await get().refreshDatasets(benchmarkId);
      return result;
    } finally {
      get().internal_updateDatasetLoading(tmpId, false);
    }
  },

  // UPDATE with optimistic update
  updateDataset: async (params) => {
    const { id, benchmarkId } = params;

    get().internal_dispatchDataset(
      {
        type: 'updateDataset',
        id,
        value: params,
      },
      benchmarkId,
    );

    get().internal_updateDatasetLoading(id, true);

    try {
      await agentEvalService.updateDataset(params);
      await get().refreshDatasets(benchmarkId);
    } finally {
      get().internal_updateDatasetLoading(id, false);
    }
  },

  // DELETE with optimistic update
  deleteDataset: async (id, benchmarkId) => {
    get().internal_dispatchDataset(
      {
        type: 'deleteDataset',
        id,
      },
      benchmarkId,
    );

    get().internal_updateDatasetLoading(id, true);

    try {
      await agentEvalService.deleteDataset(id);
      await get().refreshDatasets(benchmarkId);
    } finally {
      get().internal_updateDatasetLoading(id, false);
    }
  },

  // Internal - Dispatch to reducer
  internal_dispatchDataset: (payload, benchmarkId) => {
    const currentData = get().datasetMap[benchmarkId];
    const nextItems = datasetReducer(currentData?.items, payload);

    if (isEqual(nextItems, currentData?.items)) return;

    set(
      {
        datasetMap: {
          ...get().datasetMap,
          [benchmarkId]: {
            ...currentData,
            currentPage: currentData?.currentPage ?? 1,
            hasMore: currentData?.hasMore ?? false,
            isLoading: false,
            items: nextItems,
            pageSize: currentData?.pageSize ?? nextItems.length,
            total: currentData?.total ?? nextItems.length,
          },
        },
      },
      false,
      `dispatchDataset/${payload.type}`,
    );
  },

  // Internal - Update loading state
  internal_updateDatasetLoading: (id, loading) => {
    set(
      (state) => {
        if (loading) {
          return { loadingDatasetIds: [...state.loadingDatasetIds, id] };
        }
        return {
          loadingDatasetIds: state.loadingDatasetIds.filter((i) => i !== id),
        };
      },
      false,
      'updateDatasetLoading',
    );
  },
});

Step 3: Integrate into Store

// src/store/eval/store.ts
import { createDatasetSlice, type DatasetAction } from './slices/dataset/action';

export type EvalStore = EvalStoreState &
  BenchmarkAction &
  DatasetAction & // Add here
  RunAction;

const createStore: StateCreator<EvalStore, [['zustand/devtools', never]]> = (set, get, store) => ({
  ...initialState,
  ...createBenchmarkSlice(set, get, store),
  ...createDatasetSlice(set, get, store), // Add here
  ...createRunSlice(set, get, store),
});
// src/store/eval/initialState.ts
import { datasetInitialState, type DatasetSliceState } from './slices/dataset/initialState';

export interface EvalStoreState extends BenchmarkSliceState, DatasetSliceState {
  // ...
}

export const initialState: EvalStoreState = {
  ...benchmarkInitialState,
  ...datasetInitialState, // Add here
  ...runInitialState,
};

Step 4: Create Selectors (Optional but Recommended)

// src/store/eval/slices/dataset/selectors.ts
import type { EvalStore } from '@/store/eval/store';

export const datasetSelectors = {
  getDatasetData: (benchmarkId: string) => (s: EvalStore) => s.datasetMap[benchmarkId],

  getDatasets: (benchmarkId: string) => (s: EvalStore) => s.datasetMap[benchmarkId]?.items ?? [],

  isLoadingDataset: (id: string) => (s: EvalStore) => s.loadingDatasetIds.includes(id),
};

Step 5: Use in Component

// Component - List with Map
import { useEvalStore } from '@/store/eval';
import { datasetSelectors } from '@/store/eval/selectors';

const DatasetList = ({ benchmarkId }: { benchmarkId: string }) => {
  const useFetchDatasets = useEvalStore((s) => s.useFetchDatasets);
  const datasets = useEvalStore(datasetSelectors.getDatasets(benchmarkId));
  const datasetData = useEvalStore(datasetSelectors.getDatasetData(benchmarkId));

  useFetchDatasets(benchmarkId);

  if (datasetData?.isLoading) return <Loading />;

  return (
    <div>
      <h2>Total: {datasetData?.total ?? 0}</h2>
      <List data={datasets} />
    </div>
  );
};

// Component - Single item (for modal)
const DatasetImportModal = ({ open, datasetId }: Props) => {
  const useFetchDatasetDetail = useEvalStore((s) => s.useFetchDatasetDetail);
  const dataset = useEvalStore((s) => s.datasetDetail);
  const isLoading = useEvalStore((s) => s.isLoadingDatasetDetail);

  // Only fetch when modal is open
  useFetchDatasetDetail(open && datasetId ? datasetId : undefined);

  return (
    <Modal open={open}>
      {isLoading ? <Loading /> : <div>{dataset?.name}</div>}
    </Modal>
  );
};

Common Patterns

Pattern 1: List + Detail

// List with pagination
useFetchTestCases: (params) => {
  const { datasetId, limit, offset } = params;
  return useClientDataSWR(
    datasetId ? [FETCH_TEST_CASES_KEY, datasetId, limit, offset] : null,
    () => agentEvalService.listTestCases({ datasetId, limit, offset }),
    {
      onSuccess: (data: any) => {
        set(
          {
            testCaseList: data.data,
            testCaseTotal: data.total,
            isLoadingTestCases: false,
          },
          false,
          'useFetchTestCases/success',
        );
      },
    },
  );
};

Pattern 2: Dependent Fetching

// Component
const BenchmarkDetail = () => {
  const { benchmarkId } = useParams();

  const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail);
  const benchmark = useEvalStore((s) => s.benchmarkDetail);

  const useFetchDatasets = useEvalStore((s) => s.useFetchDatasets);
  const datasets = useEvalStore((s) => s.datasetList);

  // Fetch benchmark first
  useFetchBenchmarkDetail(benchmarkId);

  // Then fetch datasets for this benchmark
  useFetchDatasets(benchmarkId);

  return <div>...</div>;
};

Pattern 3: Conditional Fetching

// Only fetch when modal is open
const DatasetImportModal = ({ open, datasetId }: Props) => {
  const useFetchDatasetDetail = useEvalStore((s) => s.useFetchDatasetDetail);
  const dataset = useEvalStore((s) => s.datasetDetail);

  // Only fetch when open AND datasetId exists
  useFetchDatasetDetail(open && datasetId ? datasetId : undefined);

  return <Modal open={open}>...</Modal>;
};

Pattern 4: Refresh After Mutation

// Store action
createDataset: async (params) => {
  const result = await agentEvalService.createDataset(params);
  // Refresh the list after creation
  await get().refreshDatasets(params.benchmarkId);
  return result;
};

deleteDataset: async (id, benchmarkId) => {
  await agentEvalService.deleteDataset(id);
  // Refresh the list after deletion
  await get().refreshDatasets(benchmarkId);
};

Migration Guide: useEffect → Store SWR

Before (❌ Wrong)

const TestCaseList = ({ datasetId }: Props) => {
  const [data, setData] = useState<any[]>([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const result = await lambdaClient.agentEval.listTestCases.query({
          datasetId,
        });
        setData(result.data);
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [datasetId]);

  return <Table data={data} loading={loading} />;
};

After (✅ Correct)

// 1. Create service method
class AgentEvalService {
  async listTestCases(params: { datasetId: string }) {
    return lambdaClient.agentEval.listTestCases.query(params);
  }
}

// 2. Create store slice
export const createTestCaseSlice: StateCreator<...> = (set) => ({
  useFetchTestCases: (params) => {
    return useClientDataSWR(
      params.datasetId ? [FETCH_TEST_CASES_KEY, params.datasetId] : null,
      () => agentEvalService.listTestCases(params),
      {
        onSuccess: (data: any) => {
          set(
            { testCaseList: data.data, isLoadingTestCases: false },
            false,
            'useFetchTestCases/success',
          );
        },
      },
    );
  },
});

// 3. Use in component
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} />;
};

Best Practices

✅ DO

  1. Always use service layer - Never call lambdaClient directly in stores/components
  2. Use SWR hooks in stores - Not useEffect in components
  3. Clear naming - useFetchXxx for hooks, refreshXxx for cache invalidation
  4. Proper cache keys - Use constants, include parameters in array form
  5. Update state in onSuccess - Set loading states and data
  6. Refresh after mutations - Call refresh methods after create/update/delete
  7. Handle loading states - Provide loading indicators to users

❌ DON'T

  1. Don't use useEffect for data fetching
  2. Don't use useState for server data
  3. Don't call lambdaClient directly in components or stores
  4. Don't forget to refresh cache after mutations
  5. Don't duplicate state - Use store as single source of truth

Troubleshooting

Problem: Data not loading

Check:

  1. Is the hook being called? useFetchXxx()
  2. Is the key valid? (not null/undefined)
  3. Is the service method correct?
  4. Check browser network tab for API calls

Problem: Data not refreshing after mutation

Check:

  1. Did you call refreshXxx() after mutation?
  2. Is the cache key the same in both hook and refresh?
  3. Check devtools for state updates

Problem: Loading state stuck

Check:

  1. Is onSuccess updating isLoadingXxx: false?
  2. Is there an error in the API call?
  3. Check error boundary or console

Summary Checklist

When implementing new data fetching:

Step 1: Data Structures

See store-data-structures skill for detailed patterns
  • Define types in @lobechat/types:

- Detail type (e.g., AgentEvalBenchmark) - List item type (e.g., AgentEvalBenchmarkListItem)

  • Design state structure:

- List: xxxList: XxxListItem[] - Detail: xxxDetailMap: Record<string, Xxx> - Loading: loadingXxxDetailIds: string[]

  • Create reducer if optimistic updates needed

Step 2: Service Layer

  • Create service in src/services/xxxService.ts
  • Add methods:

- listXxx() - fetch list - getXxx(id) - fetch detail - createXxx(), updateXxx(), deleteXxx() - mutations

Step 3: Store Actions

  • Create initialState.ts with state structure
  • Create action.ts with:

- useFetchXxxList() - list SWR hook - useFetchXxxDetail(id) - detail SWR hook - refreshXxxList(), refreshXxxDetail(id) - cache invalidation - CRUD methods calling service - internal_dispatch and internal_updateLoading if using reducer

  • Create selectors.ts (optional but recommended)
  • Integrate slice into main store

Step 4: Component Usage

  • Use store hooks (NOT useEffect)
  • List pages: access xxxList array
  • Detail pages: access xxxDetailMap[id]
  • Use loading states for UI feedback

Remember: Types → Service → Store (SWR + Reducer) → Component 🎯

Key Architecture Patterns

  1. Service Layer: Clean API abstraction (xxxService)
  2. Data Structures: List arrays + Detail maps (see store-data-structures skill)
  3. SWR Hooks: Automatic caching and revalidation (useFetchXxx)
  4. Cache Invalidation: Manual refresh methods (refreshXxx)
  5. Optimistic Updates: Update UI immediately, then sync with server
  6. Loading States: Per-item loading for better UX

Related Skills

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

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Codex

33.57%
按下载量换算1,404

Claude

30.33%
按下载量换算1,268

Cursor

20.38%
按下载量换算852

Gemini CLI

9.28%
按下载量换算388

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills