data-fetching by lobehub/lobehub
npx skills add https://github.com/lobehub/lobehub --skill data-fetching相关技能:
store-data-structures- 如何在存储中构建列表和详情数据(Map 与 Array 模式)
┌─────────────┐
│ 组件 │
└──────┬──────┘
│ 1. 从存储调用 useFetchXxx hook
↓
┌──────────────────┐
│ Zustand 存储 │
│ (状态 + Hook) │
└──────┬───────────┘
│ 2. useClientDataSWR 调用服务
↓
┌──────────────────┐
│ 服务层 │
│ (xxxService) │
└──────┬───────────┘
│ 3. 调用 lambdaClient
↓
┌──────────────────┐
│ lambdaClient │
│ (TRPC 客户端) │
└──────────────────┘
store-data-structures 技能广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
store-data-structures 技能注意: 关于数据结构模式(Map 与 Array,列表与详情),请参阅
store-data-structures技能。
// src/services/agentEval.ts
import { lambdaClient } from '@/libs/trpc/client';
class AgentEvalService {
// 查询方法 - 读取操作
async listBenchmarks() {
return lambdaClient.agentEval.listBenchmarks.query();
}
async getBenchmark(id: string) {
return lambdaClient.agentEval.getBenchmark.query({ id });
}
// 变更方法 - 写入操作
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();
export const xxxService = new XxxService())数据结构: 关于如何构建列表和详情数据,请参阅
store-data-structures技能。
// src/store/eval/slices/benchmark/initialState.ts
import type { AgentEvalBenchmark, AgentEvalBenchmarkListItem } from '@lobechat/types';
export interface BenchmarkSliceState {
// 列表数据 - 简单数组(参见 store-data-structures 技能)
benchmarkList: AgentEvalBenchmarkListItem[];
benchmarkListInit: boolean;
// 详情数据 - 用于缓存的映射(参见 store-data-structures 技能)
benchmarkDetailMap: Record<string, AgentEvalBenchmark>;
loadingBenchmarkDetailIds: string[];
// 变更状态
isCreatingBenchmark: boolean;
isUpdatingBenchmark: boolean;
isDeletingBenchmark: boolean;
}
关于完整的 initialState、reducer 和内部 dispatch 模式,请参阅
store-data-structures技能。
// 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 - 用于数据获取
useFetchBenchmarks: () => SWRResponse;
useFetchBenchmarkDetail: (id?: string) => SWRResponse;
// 刷新方法 - 用于缓存失效
refreshBenchmarks: () => Promise<void>;
refreshBenchmarkDetail: (id: string) => Promise<void>;
// 变更动作 - 用于写操作
createBenchmark: (params: CreateParams) => Promise<any>;
updateBenchmark: (params: UpdateParams) => Promise<void>;
deleteBenchmark: (id: string) => Promise<void>;
// 内部方法 - 不直接用于 UI
internal_dispatchBenchmarkDetail: (payload: BenchmarkDetailDispatch) => void;
internal_updateBenchmarkDetailLoading: (id: string, loading: boolean) => void;
}
export const createBenchmarkSlice: StateCreator<
EvalStore,
[['zustand/devtools', never]],
[],
BenchmarkAction
> = (set, get) => ({
// 获取列表 - 简单数组
useFetchBenchmarks: () => {
return useClientDataSWR(FETCH_BENCHMARKS_KEY, () => agentEvalService.listBenchmarks(), {
onSuccess: (data: any) => {
set(
{
benchmarkList: data,
benchmarkListInit: true,
},
false,
'useFetchBenchmarks/success',
);
},
});
},
// 获取详情 - 带 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);
},
},
);
},
// 刷新方法
refreshBenchmarks: async () => {
await mutate(FETCH_BENCHMARKS_KEY);
},
refreshBenchmarkDetail: async (id) => {
await mutate([FETCH_BENCHMARK_DETAIL_KEY, id]);
},
// 创建 - 创建后刷新列表
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');
}
},
// 更新 - 对详情进行乐观更新
updateBenchmark: async (params) => {
const { id } = params;
// 1. 乐观更新
get().internal_dispatchBenchmarkDetail({
type: 'updateBenchmarkDetail',
id,
value: params,
});
// 2. 设置加载状态
get().internal_updateBenchmarkDetailLoading(id, true);
try {
// 3. 调用服务
await agentEvalService.updateBenchmark(params);
// 4. 从服务器刷新
await get().refreshBenchmarks();
await get().refreshBenchmarkDetail(id);
} finally {
get().internal_updateBenchmarkDetailLoading(id, false);
}
},
// 删除 - 刷新列表并从详情映射中移除
deleteBenchmark: async (id) => {
// 1. 乐观更新
get().internal_dispatchBenchmarkDetail({
type: 'deleteBenchmarkDetail',
id,
});
// 2. 设置加载状态
get().internal_updateBenchmarkDetailLoading(id, true);
try {
// 3. 调用服务
await agentEvalService.deleteBenchmark(id);
// 4. 刷新列表
await get().refreshBenchmarks();
} finally {
get().internal_updateBenchmarkDetailLoading(id, false);
}
},
// 内部 - 分派到 reducer(用于详情映射)
internal_dispatchBenchmarkDetail: (payload) => {
const currentMap = get().benchmarkDetailMap;
const nextMap = benchmarkDetailReducer(currentMap, payload);
// 如果映射相同则无需更新
if (isEqual(nextMap, currentMap)) return;
set({ benchmarkDetailMap: nextMap }, false, `dispatchBenchmarkDetail/${payload.type}`);
},
// 内部 - 更新特定详情的加载状态
internal_updateBenchmarkDetailLoading: (id, loading) => {
set(
(state) => {
if (loading) {
return { loadingBenchmarkDetailIds: [...state.loadingBenchmarkDetailIds, id] };
}
return {
loadingBenchmarkDetailIds: state.loadingBenchmarkDetailIds.filter((i) => i !== id),
};
},
false,
'updateBenchmarkDetailLoading',
);
},
});
mutate() 使缓存失效获取列表数据:
// 使用列表数据的组件 - ✅ 正确
import { useEvalStore } from '@/store/eval';
const BenchmarkList = () => {
// 1. 从存储中获取 hook
const useFetchBenchmarks = useEvalStore((s) => s.useFetchBenchmarks);
// 2. 获取列表数据
const benchmarks = useEvalStore((s) => s.benchmarkList);
const isInit = useEvalStore((s) => s.benchmarkListInit);
// 3. 调用 hook(SWR 处理数据获取)
useFetchBenchmarks();
// 4. 使用数据
if (!isInit) return <Loading />;
return (
<div>
<h2>总数:{benchmarks.length}</h2>
{benchmarks.map(b => <BenchmarkCard key={b.id} {...b} />)}
</div>
);
};
获取详情数据:
// 从映射中使用详情数据的组件 - ✅ 正确
import { useEvalStore } from '@/store/eval';
import { useParams } from 'react-router-dom';
const BenchmarkDetail = () => {
const { benchmarkId } = useParams<{ benchmarkId: string }>();
// 1. 获取 hook
const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail);
// 2. 从映射中获取详情
const benchmark = useEvalStore((s) =>
benchmarkId ? s.benchmarkDetailMap[benchmarkId] : undefined,
);
// 3. 获取加载状态
const isLoading = useEvalStore((s) =>
benchmarkId ? s.loadingBenchmarkDetailIds.includes(benchmarkId) : false,
);
// 4. 调用 hook
useFetchBenchmarkDetail(benchmarkId);
// 5. 使用数据
if (!benchmark) return <Loading />;
return (
<div>
<h1>{benchmark.name}</h1>
<p>{benchmark.description}</p>
{isLoading && <Spinner />}
</div>
);
};
使用选择器(推荐):
// 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),
};
// 使用选择器的组件
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>;
};
// ❌ 错误 - 不要使用 useEffect 进行数据获取
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>;
};
// 带有乐观更新的变更操作(创建/更新/删除)- ✅ 正确
import { useEvalStore } from '@/store/eval';
import { benchmarkSelectors } from '@/store/eval/selectors';
const CreateBenchmarkModal = () => {
const createBenchmark = useEvalStore((s) => s.createBenchmark);
const handleSubmit = async (values) => {
try {
// 乐观更新在 createBenchmark 内部发生
await createBenchmark(values);
message.success('创建成功');
onClose();
} catch (error) {
message.error('创建失败');
}
};
return <Form onSubmit={handleSubmit}>...</Form>;
};
// 针对特定项目的加载状态
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}>更新</button>
<button onClick={handleDelete}>删除</button>
</div>
);
};
数据结构: 关于列表与详情模式的详细比较,请参阅
store-data-structures技能。
// src/services/agentEval.ts
class AgentEvalService {
// ... 现有方法 ...
// 添加新方法
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);
}
}
// 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;
}
};
// 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 {
// 按 benchmarkId 键控的映射
datasetMap: Record<string, DatasetData>;
// 单个项目的简单状态(只读,用于模态框)
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;
// 刷新方法
refreshDatasets: (benchmarkId: string) => Promise<void>;
refreshDatasetDetail: (id: string) => Promise<void>;
// 变更操作
createDataset: (params: any) => Promise<any>;
updateDataset: (params: any) => Promise<void>;
deleteDataset: (id: string, benchmarkId: string) => Promise<void>;
// 内部方法
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) => ({
// 使用映射获取列表
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',
);
},
},
);
},
// 获取单个项目(用于模态框显示)
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]);
},
// 创建并附带乐观更新
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);
}
},
// 更新并附带乐观更新
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);
}
},
// 删除并附带乐观更新
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);
}
},
// 内部 - 分派到 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_updateDatasetLoading: (id, loading) => {
set(
(state) => {
if (loading) {
return { loadingDatasetIds: [...state.loadingDatasetIds, id] };
}
return {
loadingDatasetIds: state.loadingDatasetIds.filter((i) => i !== id),
};
},
false,
'updateDatasetLoading',
);
},
});
// src/store/eval/store.ts
import { createDatasetSlice, type DatasetAction } from './slices/dataset/action';
export type EvalStore = EvalStoreState &
BenchmarkAction &
DatasetAction & // 在此处添加
RunAction;
const createStore: StateCreator<EvalStore, [['zustand/devtools', never]]> = (set, get, store) => ({
...initialState,
...createBenchmarkSlice(set, get, store),
...createDatasetSlice(set, get, store), // 在此处添加
...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, // 在此处添加
...runInitialState,
};
// 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),
};
// 组件 - 使用映射的列表
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>总数:{datasetData?.total ?? 0}</h2>
<List data={datasets} />
</div>
);
};
// 组件 - 单个项目(用于模态框)
const DatasetImportModal = ({ open, datasetId }: Props) => {
const useFetchDatasetDetail = useEvalStore((s) => s.useFetchDatasetDetail);
const dataset = useEvalStore((s) => s.datasetDetail);
const isLoading = useEvalStore((s) => s.isLoadingDatasetDetail);
// 仅在模态框打开时获取
useFetchDatasetDetail(open && datasetId ? datasetId : undefined);
return (
<Modal open={open}>
{isLoading ? <Loading /> : <div>{dataset?.name}</div>}
</Modal>
);
};
// 带分页的列表
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',
);
},
},
);
};
// 组件
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);
// 首先获取基准测试详情
useFetchBenchmarkDetail(benchmarkId);
// 然后获取此基准测试的数据集
useFetchDatasets(benchmarkId);
return <div>...</div>;
};
// 仅在模态框打开时获取
const DatasetImportModal = ({ open, datasetId }: Props) => {
const useFetchDatasetDetail = useEvalStore((s) => s.useFetchDatasetDetail);
const dataset = useEvalStore((s) => s.datasetDetail);
// 仅在打开且 datasetId 存在时获取
useFetchDatasetDetail(open && datasetId ? datasetId : undefined);
return <Modal open={open}>...</Modal>;
};
// 存储动作
createDataset: async (params) => {
const result = await agentEvalService.createDataset(params);
// 创建后刷新列表
await get().refreshDatasets(params.benchmarkId);
return result;
};
deleteDataset: async (id, benchmarkId) => {
await agentEvalService.deleteDataset(id);
// 删除后刷新列表
await get().refreshDatasets(benchmarkId);
};
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} />;
};
// 1. 创建服务方法
class AgentEvalService {
async listTestCases(params: { datasetId: string }) {
return lambdaClient.agentEval.listTestCases.query(params);
}
}
// 2. 创建存储切片
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. 在组件中使用
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} />;
};
useFetchXxx 用于 hooks,refreshXxx 用于缓存失效检查:
useFetchXxx()检查:
refreshXxx()?检查:
onSuccess 是否更新了 isLoadingXxx: false?实现新数据获取时:
关于详细模式,请参阅
store-data-structures技能
@lobechat/types 中定义类型:
AgentEvalBenchmark)AgentEvalBenchmarkListItem)xxxList: XxxListItem[]xxxDetailMap: Record<string, Xxx>loadingXxxDetailIds: string[]src/services/xxxService.ts 中Related Skills:
store-data-structures- How to structure List and Detail data in stores (Map vs Array patterns)
┌─────────────┐
│ Component │
└──────┬──────┘
│ 1. Call useFetchXxx hook from store
↓
┌──────────────────┐
│ Zustand Store │
│ (State + Hook) │
└──────┬───────────┘
│ 2. useClientDataSWR calls service
↓
┌──────────────────┐
│ Service Layer │
│ (xxxService) │
└──────┬───────────┘
│ 3. Call lambdaClient
↓
┌──────────────────┐
│ lambdaClient │
│ (TRPC Client) │
└──────────────────┘
store-data-structures skill for List vs Detail patternsstore-data-structures skillNote: For data structure patterns (Map vs Array, List vs Detail), see the
store-data-structuresskill.
// 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();
export const xxxService = new XxxService())Data Structure: See
store-data-structuresskill for how to structure List and Detail data.
// 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-structuresskill.
// 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',
);
},
});
mutate() to invalidate cacheFetching 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>;
};
// ❌ 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 (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-structuresskill.
// 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);
}
}
// 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;
}
};
// 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',
);
},
});
// 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,
};
// 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),
};
// 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>
);
};
// 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',
);
},
},
);
};
// 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>;
};
// 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>;
};
// 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);
};
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} />;
};
// 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} />;
};
useFetchXxx for hooks, refreshXxx for cache invalidationCheck:
useFetchXxx()Check:
refreshXxx() after mutation?Check:
onSuccess updating isLoadingXxx: false?When implementing new data fetching:
See
store-data-structuresskill for detailed patterns
@lobechat/types:
AgentEvalBenchmark)AgentEvalBenchmarkListItem)xxxList: XxxListItem[]xxxDetailMap: Record<string, Xxx>loadingXxxDetailIds: string[]src/services/xxxService.tslistXxx() - fetch listgetXxx(id) - fetch detailcreateXxx(), updateXxx(), deleteXxx() - mutationsinitialState.ts with state structureaction.ts with:
useFetchXxxList() - list SWR hookuseFetchXxxDetail(id) - detail SWR hookrefreshXxxList(), refreshXxxDetail(id) - cache invalidationinternal_dispatch and internal_updateLoading if using reducerselectors.ts (optional but recommended)xxxList arrayxxxDetailMap[id]Remember: Types → Service → Store (SWR + Reducer) → Component 🎯
xxxService)store-data-structures skill)useFetchXxx)refreshXxx)store-data-structures - How to structure List and Detail data in storeszustand - General Zustand patterns and best practicesWeekly Installs
201
Repository
GitHub Stars
74.2K
First Seen
Feb 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex200
github-copilot200
kimi-cli199
gemini-cli199
cursor199
opencode199
React Native 棕地迁移指南:逐步集成到原生应用(Expo/裸RN)
465 周安装