<< All versions
Skill v1.0.0
currentAutomated scan100/100andershi666/lobehub-hr-demo/data-fetching
──Details
PublishedApril 29, 2026 at 02:07 PM
Content Hashsha256:001c135990eca992...
Git SHAc32ae54d58f1
──Files
Files (1 file, 31.4 KB)
SKILL.md31.4 KBactive
SKILL.md · 1177 lines · 31.4 KB
version: "1.0.0" name: data-fetching description: 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.
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
- Use Service Layer for all API calls
- Use Store SWR Hooks for data fetching (not useEffect)
- Use proper data structures - See
store-data-structuresskill for List vs Detail patterns - Use lambdaClient.mutate for write operations (create/update/delete)
- Use lambdaClient.query only inside service methods
❌ DON'T
- Never use useEffect for data fetching
- Never call lambdaClient directly in components or stores
- Never use useState for server data
- Never mix data structure patterns - Follow
store-data-structuresskill
Note: For data structure patterns (Map vs Array, List vs Detail), see thestore-data-structuresskill.
Layer 1: Service Layer
Purpose
- Encapsulate all API calls to lambdaClient
- Provide clean, typed interfaces
- Single source of truth for API operations
Service Structure
typescript
// src/services/agentEval.tsimport { lambdaClient } from '@/libs/trpc/client';class AgentEvalService {// Query methods - READ operationsasync listBenchmarks() {return lambdaClient.agentEval.listBenchmarks.query();}async getBenchmark(id: string) {return lambdaClient.agentEval.getBenchmark.query({ id });}// Mutation methods - WRITE operationsasync 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
- One service per domain (e.g., agentEval, ragEval, aiAgent)
- Export singleton instance (
export const xxxService = new XxxService()) - Method names match operations (list, get, create, update, delete)
- 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: Seestore-data-structuresskill for how to structure List and Detail data.
Store Structure Overview
typescript
// src/store/eval/slices/benchmark/initialState.tsimport 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 statesisCreatingBenchmark: boolean;isUpdatingBenchmark: boolean;isDeletingBenchmark: boolean;}
For complete initialState, reducer, and internal dispatch patterns, see thestore-data-structuresskill.
Create Actions
typescript
// src/store/eval/slices/benchmark/action.tsimport 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 fetchinguseFetchBenchmarks: () => SWRResponse;useFetchBenchmarkDetail: (id?: string) => SWRResponse;// Refresh methods - for cache invalidationrefreshBenchmarks: () => Promise<void>;refreshBenchmarkDetail: (id: string) => Promise<void>;// Mutation actions - for write operationscreateBenchmark: (params: CreateParams) => Promise<any>;updateBenchmark: (params: UpdateParams) => Promise<void>;deleteBenchmark: (id: string) => Promise<void>;// Internal methods - not for direct UI useinternal_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 arrayuseFetchBenchmarks: () => {return useClientDataSWR(FETCH_BENCHMARKS_KEY, () => agentEvalService.listBenchmarks(), {onSuccess: (data: any) => {set({benchmarkList: data,benchmarkListInit: true,},false,'useFetchBenchmarks/success',);},});},// Fetch detail - Map with dispatchuseFetchBenchmarkDetail: (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 methodsrefreshBenchmarks: async () => {await mutate(FETCH_BENCHMARKS_KEY);},refreshBenchmarkDetail: async (id) => {await mutate([FETCH_BENCHMARK_DETAIL_KEY, id]);},// CREATE - Refresh list after creationcreateBenchmark: 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 detailupdateBenchmark: async (params) => {const { id } = params;// 1. Optimistic updateget().internal_dispatchBenchmarkDetail({type: 'updateBenchmarkDetail',id,value: params,});// 2. Set loadingget().internal_updateBenchmarkDetailLoading(id, true);try {// 3. Call serviceawait agentEvalService.updateBenchmark(params);// 4. Refresh from serverawait get().refreshBenchmarks();await get().refreshBenchmarkDetail(id);} finally {get().internal_updateBenchmarkDetailLoading(id, false);}},// DELETE - Refresh list and remove from detail mapdeleteBenchmark: async (id) => {// 1. Optimistic updateget().internal_dispatchBenchmarkDetail({type: 'deleteBenchmarkDetail',id,});// 2. Set loadingget().internal_updateBenchmarkDetailLoading(id, true);try {// 3. Call serviceawait agentEvalService.deleteBenchmark(id);// 4. Refresh listawait 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 sameif (isEqual(nextMap, currentMap)) return;set({ benchmarkDetailMap: nextMap }, false, `dispatchBenchmarkDetail/${payload.type}`);},// Internal - Update loading state for specific detailinternal_updateBenchmarkDetailLoading: (id, loading) => {set((state) => {if (loading) {return { loadingBenchmarkDetailIds: [...state.loadingBenchmarkDetailIds, id] };}return {loadingBenchmarkDetailIds: state.loadingBenchmarkDetailIds.filter((i) => i !== id),};},false,'updateBenchmarkDetailLoading',);},});
Store Guidelines
- SWR keys as constants at top of file
- useClientDataSWR for all data fetching (never useEffect)
- onSuccess callback updates store state
- Refresh methods use
mutate()to invalidate cache - Loading states in initialState, updated in onSuccess
- Mutations call service, then refresh relevant cache
Layer 3: Component Usage
Data Fetching in Components
Fetching List Data:
typescript
// Component using list data - ✅ CORRECTimport { useEvalStore } from '@/store/eval';const BenchmarkList = () => {// 1. Get the hook from storeconst useFetchBenchmarks = useEvalStore((s) => s.useFetchBenchmarks);// 2. Get list dataconst benchmarks = useEvalStore((s) => s.benchmarkList);const isInit = useEvalStore((s) => s.benchmarkListInit);// 3. Call the hook (SWR handles the data fetching)useFetchBenchmarks();// 4. Use the dataif (!isInit) return <Loading />;return (<div><h2>Total: {benchmarks.length}</h2>{benchmarks.map(b => <BenchmarkCard key={b.id} {...b} />)}</div>);};
Fetching Detail Data:
typescript
// Component using detail data from map - ✅ CORRECTimport { useEvalStore } from '@/store/eval';import { useParams } from 'react-router-dom';const BenchmarkDetail = () => {const { benchmarkId } = useParams<{ benchmarkId: string }>();// 1. Get the hookconst useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail);// 2. Get detail from mapconst benchmark = useEvalStore((s) =>benchmarkId ? s.benchmarkDetailMap[benchmarkId] : undefined,);// 3. Get loading stateconst isLoading = useEvalStore((s) =>benchmarkId ? s.loadingBenchmarkDetailIds.includes(benchmarkId) : false,);// 4. Call the hookuseFetchBenchmarkDetail(benchmarkId);// 5. Use the dataif (!benchmark) return <Loading />;return (<div><h1>{benchmark.name}</h1><p>{benchmark.description}</p>{isLoading && <Spinner />}</div>);};
Using Selectors (Recommended):
typescript
// src/store/eval/slices/benchmark/selectors.tsexport const benchmarkSelectors = {getBenchmarkDetail: (id: string) => (s: EvalStore) => s.benchmarkDetailMap[id],isLoadingBenchmarkDetail: (id: string) => (s: EvalStore) =>s.loadingBenchmarkDetailIds.includes(id),};// Component with selectorsconst 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
typescript
// ❌ WRONG - Don't use useEffect for data fetchingconst 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
typescript
// Mutations (Create/Update/Delete) with optimistic updates - ✅ CORRECTimport { 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 createBenchmarkawait createBenchmark(values);message.success('Created successfully');onClose();} catch (error) {message.error('Failed to create');}};return <Form onSubmit={handleSubmit}>...</Form>;};// With loading state for specific itemconst 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 thestore-data-structuresskill.
Complete Example: Adding a New Feature
Scenario: Add "Dataset" data fetching with optimistic updates
Step 1: Create Service
typescript
// src/services/agentEval.tsclass AgentEvalService {// ... existing methods ...// Add new methodsasync 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
typescript
// src/store/eval/slices/dataset/reducer.tsimport { 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
typescript
// src/store/eval/slices/dataset/initialState.tsimport 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 benchmarkIddatasetMap: 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: [],};
typescript
// src/store/eval/slices/dataset/action.tsimport 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 HooksuseFetchDatasets: (benchmarkId?: string) => SWRResponse;useFetchDatasetDetail: (id?: string) => SWRResponse;// Refresh methodsrefreshDatasets: (benchmarkId: string) => Promise<void>;refreshDatasetDetail: (id: string) => Promise<void>;// MutationscreateDataset: (params: any) => Promise<any>;updateDataset: (params: any) => Promise<void>;deleteDataset: (id: string, benchmarkId: string) => Promise<void>;// Internal methodsinternal_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 MapuseFetchDatasets: (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 updatecreateDataset: 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 updateupdateDataset: 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 updatedeleteDataset: 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 reducerinternal_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 stateinternal_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
typescript
// src/store/eval/store.tsimport { createDatasetSlice, type DatasetAction } from './slices/dataset/action';export type EvalStore = EvalStoreState &BenchmarkAction &DatasetAction & // Add hereRunAction;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),});
typescript
// src/store/eval/initialState.tsimport { 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)
typescript
// src/store/eval/slices/dataset/selectors.tsimport 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
typescript
// Component - List with Mapimport { 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 openuseFetchDatasetDetail(open && datasetId ? datasetId : undefined);return (<Modal open={open}>{isLoading ? <Loading /> : <div>{dataset?.name}</div>}</Modal>);};
Common Patterns
Pattern 1: List + Detail
typescript
// List with paginationuseFetchTestCases: (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
typescript
// Componentconst 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 firstuseFetchBenchmarkDetail(benchmarkId);// Then fetch datasets for this benchmarkuseFetchDatasets(benchmarkId);return <div>...</div>;};
Pattern 3: Conditional Fetching
typescript
// Only fetch when modal is openconst DatasetImportModal = ({ open, datasetId }: Props) => {const useFetchDatasetDetail = useEvalStore((s) => s.useFetchDatasetDetail);const dataset = useEvalStore((s) => s.datasetDetail);// Only fetch when open AND datasetId existsuseFetchDatasetDetail(open && datasetId ? datasetId : undefined);return <Modal open={open}>...</Modal>;};
Pattern 4: Refresh After Mutation
typescript
// Store actioncreateDataset: async (params) => {const result = await agentEvalService.createDataset(params);// Refresh the list after creationawait get().refreshDatasets(params.benchmarkId);return result;};deleteDataset: async (id, benchmarkId) => {await agentEvalService.deleteDataset(id);// Refresh the list after deletionawait get().refreshDatasets(benchmarkId);};
Migration Guide: useEffect → Store SWR
Before (❌ Wrong)
typescript
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)
typescript
// 1. Create service methodclass AgentEvalService {async listTestCases(params: { datasetId: string }) {return lambdaClient.agentEval.listTestCases.query(params);}}// 2. Create store sliceexport 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 componentconst 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
- Always use service layer - Never call lambdaClient directly in stores/components
- Use SWR hooks in stores - Not useEffect in components
- Clear naming -
useFetchXxxfor hooks,refreshXxxfor cache invalidation - Proper cache keys - Use constants, include parameters in array form
- Update state in onSuccess - Set loading states and data
- Refresh after mutations - Call refresh methods after create/update/delete
- Handle loading states - Provide loading indicators to users
❌ DON'T
- Don't use useEffect for data fetching
- Don't use useState for server data
- Don't call lambdaClient directly in components or stores
- Don't forget to refresh cache after mutations
- Don't duplicate state - Use store as single source of truth
Troubleshooting
Problem: Data not loading
Check:
- Is the hook being called?
useFetchXxx() - Is the key valid? (not null/undefined)
- Is the service method correct?
- Check browser network tab for API calls
Problem: Data not refreshing after mutation
Check:
- Did you call
refreshXxx()after mutation? - Is the cache key the same in both hook and refresh?
- Check devtools for state updates
Problem: Loading state stuck
Check:
- Is
onSuccessupdatingisLoadingXxx: false? - Is there an error in the API call?
- Check error boundary or console
Summary Checklist
When implementing new data fetching:
Step 1: Data Structures
Seestore-data-structuresskill 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.tswith state structure - [ ] Create
action.tswith: - [ ]
useFetchXxxList()- list SWR hook - [ ]
useFetchXxxDetail(id)- detail SWR hook - [ ]
refreshXxxList(),refreshXxxDetail(id)- cache invalidation - [ ] CRUD methods calling service
- [ ]
internal_dispatchandinternal_updateLoadingif 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
xxxListarray - [ ] Detail pages: access
xxxDetailMap[id] - [ ] Use loading states for UI feedback
Remember: Types → Service → Store (SWR + Reducer) → Component 🎯
Key Architecture Patterns
- Service Layer: Clean API abstraction (
xxxService) - Data Structures: List arrays + Detail maps (see
store-data-structuresskill) - SWR Hooks: Automatic caching and revalidation (
useFetchXxx) - Cache Invalidation: Manual refresh methods (
refreshXxx) - Optimistic Updates: Update UI immediately, then sync with server
- 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