Loading...
Loading...
Data fetching architecture guide using Service layer + Zustand Store + SWR. Use when implementing data fetching, creating services, working with store hooks, or migrating from useEffect. Triggers on data loading, API calls, service creation, or store data fetching tasks.
npx skill4agent add lobehub/lobehub data-fetchingRelated Skills:
- How to structure List and Detail data in stores (Map vs Array patterns)store-data-structures
┌─────────────┐
│ 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-structuresstore-data-structuresNote: For data structure patterns (Map vs Array, List vs Detail), see theskill.store-data-structures
// 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: Seeskill for how to structure List and Detail data.store-data-structures
// 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 theskill.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 - 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()// 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>
);
};// 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>
);
};// 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 theskill.store-data-structures
// 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} />;
};useFetchXxxrefreshXxxuseFetchXxx()refreshXxx()onSuccessisLoadingXxx: falseSeeskill for detailed patternsstore-data-structures
@lobechat/typesAgentEvalBenchmarkAgentEvalBenchmarkListItemxxxList: XxxListItem[]xxxDetailMap: Record<string, Xxx>loadingXxxDetailIds: string[]src/services/xxxService.tslistXxx()getXxx(id)createXxx()updateXxx()deleteXxx()initialState.tsaction.tsuseFetchXxxList()useFetchXxxDetail(id)refreshXxxList()refreshXxxDetail(id)internal_dispatchinternal_updateLoadingselectors.tsxxxListxxxDetailMap[id]xxxServicestore-data-structuresuseFetchXxxrefreshXxxstore-data-structureszustand