Awesome-omni-skill data-fetching
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.
install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data-ai/data-fetching-neversight" ~/.claude/skills/diegosouzapw-awesome-omni-skill-data-fetching && rm -rf "$T"
manifest:
skills/data-ai/data-fetching-neversight/SKILL.mdsource content
LobeHub Data Fetching Architecture
Related Skills:
- How to structure List and Detail data in stores (Map vs Array patterns)store-data-structures
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
skill for List vs Detail patternsstore-data-structures - 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
skillstore-data-structures
Note: For data structure patterns (Map vs Array, List vs Detail), see the
skill.store-data-structures
Layer 1: Service Layer
Purpose
- Encapsulate all API calls to lambdaClient
- Provide clean, typed interfaces
- Single source of truth for API operations
Service Structure
// src/services/agentEval.ts import { lambdaClient } from '@/libs/trpc/client'; class AgentEvalService { // Query methods - READ operations async listBenchmarks() { return lambdaClient.agentEval.listBenchmarks.query(); } async getBenchmark(id: string) { return lambdaClient.agentEval.getBenchmark.query({ id }); } // Mutation methods - WRITE operations async createBenchmark(params: CreateBenchmarkParams) { return lambdaClient.agentEval.createBenchmark.mutate(params); } async updateBenchmark(params: UpdateBenchmarkParams) { return lambdaClient.agentEval.updateBenchmark.mutate(params); } async deleteBenchmark(id: string) { return lambdaClient.agentEval.deleteBenchmark.mutate({ id }); } } export const agentEvalService = new AgentEvalService();
Service Guidelines
- 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: See
skill for how to structure List and Detail data.store-data-structures
Store Structure Overview
// src/store/eval/slices/benchmark/initialState.ts import type { AgentEvalBenchmark, AgentEvalBenchmarkListItem } from '@lobechat/types'; export interface BenchmarkSliceState { // List data - simple array (see store-data-structures skill) benchmarkList: AgentEvalBenchmarkListItem[]; benchmarkListInit: boolean; // Detail data - map for caching (see store-data-structures skill) benchmarkDetailMap: Record<string, AgentEvalBenchmark>; loadingBenchmarkDetailIds: string[]; // Mutation states isCreatingBenchmark: boolean; isUpdatingBenchmark: boolean; isDeletingBenchmark: boolean; }
For complete initialState, reducer, and internal dispatch patterns, see the
skill.store-data-structures
Create Actions
// src/store/eval/slices/benchmark/action.ts import type { SWRResponse } from 'swr'; import type { StateCreator } from 'zustand/vanilla'; import isEqual from 'fast-deep-equal'; import { mutate, useClientDataSWR } from '@/libs/swr'; import { agentEvalService } from '@/services/agentEval'; import type { EvalStore } from '@/store/eval/store'; import { benchmarkDetailReducer, type BenchmarkDetailDispatch } from './reducer'; const FETCH_BENCHMARKS_KEY = 'FETCH_BENCHMARKS'; const FETCH_BENCHMARK_DETAIL_KEY = 'FETCH_BENCHMARK_DETAIL'; export interface BenchmarkAction { // SWR Hooks - for data fetching useFetchBenchmarks: () => SWRResponse; useFetchBenchmarkDetail: (id?: string) => SWRResponse; // Refresh methods - for cache invalidation refreshBenchmarks: () => Promise<void>; refreshBenchmarkDetail: (id: string) => Promise<void>; // Mutation actions - for write operations createBenchmark: (params: CreateParams) => Promise<any>; updateBenchmark: (params: UpdateParams) => Promise<void>; deleteBenchmark: (id: string) => Promise<void>; // Internal methods - not for direct UI use internal_dispatchBenchmarkDetail: (payload: BenchmarkDetailDispatch) => void; internal_updateBenchmarkDetailLoading: (id: string, loading: boolean) => void; } export const createBenchmarkSlice: StateCreator< EvalStore, [['zustand/devtools', never]], [], BenchmarkAction > = (set, get) => ({ // Fetch list - Simple array useFetchBenchmarks: () => { return useClientDataSWR(FETCH_BENCHMARKS_KEY, () => agentEvalService.listBenchmarks(), { onSuccess: (data: any) => { set( { benchmarkList: data, benchmarkListInit: true, }, false, 'useFetchBenchmarks/success', ); }, }); }, // Fetch detail - Map with dispatch useFetchBenchmarkDetail: (id) => { return useClientDataSWR( id ? [FETCH_BENCHMARK_DETAIL_KEY, id] : null, () => agentEvalService.getBenchmark(id!), { onSuccess: (data: any) => { get().internal_dispatchBenchmarkDetail({ type: 'setBenchmarkDetail', id: id!, value: data, }); get().internal_updateBenchmarkDetailLoading(id!, false); }, }, ); }, // Refresh methods refreshBenchmarks: async () => { await mutate(FETCH_BENCHMARKS_KEY); }, refreshBenchmarkDetail: async (id) => { await mutate([FETCH_BENCHMARK_DETAIL_KEY, id]); }, // CREATE - Refresh list after creation createBenchmark: async (params) => { set({ isCreatingBenchmark: true }, false, 'createBenchmark/start'); try { const result = await agentEvalService.createBenchmark(params); await get().refreshBenchmarks(); return result; } finally { set({ isCreatingBenchmark: false }, false, 'createBenchmark/end'); } }, // UPDATE - With optimistic update for detail updateBenchmark: async (params) => { const { id } = params; // 1. Optimistic update get().internal_dispatchBenchmarkDetail({ type: 'updateBenchmarkDetail', id, value: params, }); // 2. Set loading get().internal_updateBenchmarkDetailLoading(id, true); try { // 3. Call service await agentEvalService.updateBenchmark(params); // 4. Refresh from server await get().refreshBenchmarks(); await get().refreshBenchmarkDetail(id); } finally { get().internal_updateBenchmarkDetailLoading(id, false); } }, // DELETE - Refresh list and remove from detail map deleteBenchmark: async (id) => { // 1. Optimistic update get().internal_dispatchBenchmarkDetail({ type: 'deleteBenchmarkDetail', id, }); // 2. Set loading get().internal_updateBenchmarkDetailLoading(id, true); try { // 3. Call service await agentEvalService.deleteBenchmark(id); // 4. Refresh list await get().refreshBenchmarks(); } finally { get().internal_updateBenchmarkDetailLoading(id, false); } }, // Internal - Dispatch to reducer (for detail map) internal_dispatchBenchmarkDetail: (payload) => { const currentMap = get().benchmarkDetailMap; const nextMap = benchmarkDetailReducer(currentMap, payload); // No need to update if map is the same if (isEqual(nextMap, currentMap)) return; set({ benchmarkDetailMap: nextMap }, false, `dispatchBenchmarkDetail/${payload.type}`); }, // Internal - Update loading state for specific detail internal_updateBenchmarkDetailLoading: (id, loading) => { set( (state) => { if (loading) { return { loadingBenchmarkDetailIds: [...state.loadingBenchmarkDetailIds, id] }; } return { loadingBenchmarkDetailIds: state.loadingBenchmarkDetailIds.filter((i) => i !== id), }; }, false, 'updateBenchmarkDetailLoading', ); }, });
Store Guidelines
- SWR keys as constants at top of file
- useClientDataSWR for all data fetching (never useEffect)
- onSuccess callback updates store state
- Refresh methods use
to invalidate cachemutate() - 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:
// Component using list data - ✅ CORRECT import { useEvalStore } from '@/store/eval'; const BenchmarkList = () => { // 1. Get the hook from store const useFetchBenchmarks = useEvalStore((s) => s.useFetchBenchmarks); // 2. Get list data const benchmarks = useEvalStore((s) => s.benchmarkList); const isInit = useEvalStore((s) => s.benchmarkListInit); // 3. Call the hook (SWR handles the data fetching) useFetchBenchmarks(); // 4. Use the data if (!isInit) return <Loading />; return ( <div> <h2>Total: {benchmarks.length}</h2> {benchmarks.map(b => <BenchmarkCard key={b.id} {...b} />)} </div> ); };
Fetching Detail Data:
// Component using detail data from map - ✅ CORRECT import { useEvalStore } from '@/store/eval'; import { useParams } from 'react-router-dom'; const BenchmarkDetail = () => { const { benchmarkId } = useParams<{ benchmarkId: string }>(); // 1. Get the hook const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail); // 2. Get detail from map const benchmark = useEvalStore((s) => benchmarkId ? s.benchmarkDetailMap[benchmarkId] : undefined, ); // 3. Get loading state const isLoading = useEvalStore((s) => benchmarkId ? s.loadingBenchmarkDetailIds.includes(benchmarkId) : false, ); // 4. Call the hook useFetchBenchmarkDetail(benchmarkId); // 5. Use the data if (!benchmark) return <Loading />; return ( <div> <h1>{benchmark.name}</h1> <p>{benchmark.description}</p> {isLoading && <Spinner />} </div> ); };
Using Selectors (Recommended):
// src/store/eval/slices/benchmark/selectors.ts export const benchmarkSelectors = { getBenchmarkDetail: (id: string) => (s: EvalStore) => s.benchmarkDetailMap[id], isLoadingBenchmarkDetail: (id: string) => (s: EvalStore) => s.loadingBenchmarkDetailIds.includes(id), }; // Component with selectors const BenchmarkDetail = () => { const { benchmarkId } = useParams(); const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail); const benchmark = useEvalStore(benchmarkSelectors.getBenchmarkDetail(benchmarkId!)); useFetchBenchmarkDetail(benchmarkId); return <div>{benchmark && <h1>{benchmark.name}</h1>}</div>; };
What NOT to Do
// ❌ WRONG - Don't use useEffect for data fetching const BenchmarkList = () => { const [data, setData] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { const fetchData = async () => { setLoading(true); const result = await lambdaClient.agentEval.listBenchmarks.query(); setData(result); setLoading(false); }; fetchData(); }, []); return <div>...</div>; };
Mutations in Components
// Mutations (Create/Update/Delete) with optimistic updates - ✅ CORRECT import { useEvalStore } from '@/store/eval'; import { benchmarkSelectors } from '@/store/eval/selectors'; const CreateBenchmarkModal = () => { const createBenchmark = useEvalStore((s) => s.createBenchmark); const handleSubmit = async (values) => { try { // Optimistic update happens inside createBenchmark await createBenchmark(values); message.success('Created successfully'); onClose(); } catch (error) { message.error('Failed to create'); } }; return <Form onSubmit={handleSubmit}>...</Form>; }; // With loading state for specific item const BenchmarkItem = ({ id }: { id: string }) => { const updateBenchmark = useEvalStore((s) => s.updateBenchmark); const deleteBenchmark = useEvalStore((s) => s.deleteBenchmark); const isLoading = useEvalStore(benchmarkSelectors.isLoadingBenchmark(id)); const handleUpdate = async (data) => { await updateBenchmark({ id, ...data }); }; const handleDelete = async () => { await deleteBenchmark(id); }; return ( <div> {isLoading && <Spinner />} <button onClick={handleUpdate}>Update</button> <button onClick={handleDelete}>Delete</button> </div> ); };
Data Structures: For detailed comparison of List vs Detail patterns, see the
skill.store-data-structures
Complete Example: Adding a New Feature
Scenario: Add "Dataset" data fetching with optimistic updates
Step 1: Create Service
// src/services/agentEval.ts class AgentEvalService { // ... existing methods ... // Add new methods async listDatasets(benchmarkId: string) { return lambdaClient.agentEval.listDatasets.query({ benchmarkId }); } async getDataset(id: string) { return lambdaClient.agentEval.getDataset.query({ id }); } async createDataset(params: CreateDatasetParams) { return lambdaClient.agentEval.createDataset.mutate(params); } }
Step 2: Create Reducer
// src/store/eval/slices/dataset/reducer.ts import { produce } from 'immer'; import type { Dataset } from '@/types/dataset'; type AddDatasetAction = { type: 'addDataset'; value: Dataset; }; type UpdateDatasetAction = { id: string; type: 'updateDataset'; value: Partial<Dataset>; }; type DeleteDatasetAction = { id: string; type: 'deleteDataset'; }; export type DatasetDispatch = AddDatasetAction | UpdateDatasetAction | DeleteDatasetAction; export const datasetReducer = (state: Dataset[] = [], payload: DatasetDispatch): Dataset[] => { switch (payload.type) { case 'addDataset': { return produce(state, (draft) => { draft.unshift(payload.value); }); } case 'updateDataset': { return produce(state, (draft) => { const index = draft.findIndex((item) => item.id === payload.id); if (index !== -1) { draft[index] = { ...draft[index], ...payload.value }; } }); } case 'deleteDataset': { return produce(state, (draft) => { const index = draft.findIndex((item) => item.id === payload.id); if (index !== -1) { draft.splice(index, 1); } }); } default: return state; } };
Step 3: Create Store Slice
// src/store/eval/slices/dataset/initialState.ts import type { Dataset } from '@/types/dataset'; export interface DatasetData { currentPage: number; hasMore: boolean; isLoading: boolean; items: Dataset[]; pageSize: number; total: number; } export interface DatasetSliceState { // Map keyed by benchmarkId datasetMap: Record<string, DatasetData>; // Simple state for single item (read-only, used in modals) datasetDetail: Dataset | null; isLoadingDatasetDetail: boolean; loadingDatasetIds: string[]; } export const datasetInitialState: DatasetSliceState = { datasetMap: {}, datasetDetail: null, isLoadingDatasetDetail: false, loadingDatasetIds: [], };
// src/store/eval/slices/dataset/action.ts import type { SWRResponse } from 'swr'; import type { StateCreator } from 'zustand/vanilla'; import isEqual from 'fast-deep-equal'; import { mutate, useClientDataSWR } from '@/libs/swr'; import { agentEvalService } from '@/services/agentEval'; import type { EvalStore } from '@/store/eval/store'; import { datasetReducer, type DatasetDispatch } from './reducer'; const FETCH_DATASETS_KEY = 'FETCH_DATASETS'; const FETCH_DATASET_DETAIL_KEY = 'FETCH_DATASET_DETAIL'; export interface DatasetAction { // SWR Hooks useFetchDatasets: (benchmarkId?: string) => SWRResponse; useFetchDatasetDetail: (id?: string) => SWRResponse; // Refresh methods refreshDatasets: (benchmarkId: string) => Promise<void>; refreshDatasetDetail: (id: string) => Promise<void>; // Mutations createDataset: (params: any) => Promise<any>; updateDataset: (params: any) => Promise<void>; deleteDataset: (id: string, benchmarkId: string) => Promise<void>; // Internal methods internal_dispatchDataset: (payload: DatasetDispatch, benchmarkId: string) => void; internal_updateDatasetLoading: (id: string, loading: boolean) => void; } export const createDatasetSlice: StateCreator< EvalStore, [['zustand/devtools', never]], [], DatasetAction > = (set, get) => ({ // Fetch list with Map useFetchDatasets: (benchmarkId) => { return useClientDataSWR( benchmarkId ? [FETCH_DATASETS_KEY, benchmarkId] : null, () => agentEvalService.listDatasets(benchmarkId!), { onSuccess: (data: any) => { set( { datasetMap: { ...get().datasetMap, [benchmarkId!]: { currentPage: 1, hasMore: false, isLoading: false, items: data, pageSize: data.length, total: data.length, }, }, }, false, 'useFetchDatasets/success', ); }, }, ); }, // Fetch single item (for modal display) useFetchDatasetDetail: (id) => { return useClientDataSWR( id ? [FETCH_DATASET_DETAIL_KEY, id] : null, () => agentEvalService.getDataset(id!), { onSuccess: (data: any) => { set( { datasetDetail: data, isLoadingDatasetDetail: false }, false, 'useFetchDatasetDetail/success', ); }, }, ); }, refreshDatasets: async (benchmarkId) => { await mutate([FETCH_DATASETS_KEY, benchmarkId]); }, refreshDatasetDetail: async (id) => { await mutate([FETCH_DATASET_DETAIL_KEY, id]); }, // CREATE with optimistic update createDataset: async (params) => { const tmpId = Date.now().toString(); const { benchmarkId } = params; get().internal_dispatchDataset( { type: 'addDataset', value: { ...params, id: tmpId, createdAt: Date.now() } as any, }, benchmarkId, ); get().internal_updateDatasetLoading(tmpId, true); try { const result = await agentEvalService.createDataset(params); await get().refreshDatasets(benchmarkId); return result; } finally { get().internal_updateDatasetLoading(tmpId, false); } }, // UPDATE with optimistic update updateDataset: async (params) => { const { id, benchmarkId } = params; get().internal_dispatchDataset( { type: 'updateDataset', id, value: params, }, benchmarkId, ); get().internal_updateDatasetLoading(id, true); try { await agentEvalService.updateDataset(params); await get().refreshDatasets(benchmarkId); } finally { get().internal_updateDatasetLoading(id, false); } }, // DELETE with optimistic update deleteDataset: async (id, benchmarkId) => { get().internal_dispatchDataset( { type: 'deleteDataset', id, }, benchmarkId, ); get().internal_updateDatasetLoading(id, true); try { await agentEvalService.deleteDataset(id); await get().refreshDatasets(benchmarkId); } finally { get().internal_updateDatasetLoading(id, false); } }, // Internal - Dispatch to reducer internal_dispatchDataset: (payload, benchmarkId) => { const currentData = get().datasetMap[benchmarkId]; const nextItems = datasetReducer(currentData?.items, payload); if (isEqual(nextItems, currentData?.items)) return; set( { datasetMap: { ...get().datasetMap, [benchmarkId]: { ...currentData, currentPage: currentData?.currentPage ?? 1, hasMore: currentData?.hasMore ?? false, isLoading: false, items: nextItems, pageSize: currentData?.pageSize ?? nextItems.length, total: currentData?.total ?? nextItems.length, }, }, }, false, `dispatchDataset/${payload.type}`, ); }, // Internal - Update loading state internal_updateDatasetLoading: (id, loading) => { set( (state) => { if (loading) { return { loadingDatasetIds: [...state.loadingDatasetIds, id] }; } return { loadingDatasetIds: state.loadingDatasetIds.filter((i) => i !== id), }; }, false, 'updateDatasetLoading', ); }, });
Step 3: Integrate into Store
// src/store/eval/store.ts import { createDatasetSlice, type DatasetAction } from './slices/dataset/action'; export type EvalStore = EvalStoreState & BenchmarkAction & DatasetAction & // Add here RunAction; const createStore: StateCreator<EvalStore, [['zustand/devtools', never]]> = (set, get, store) => ({ ...initialState, ...createBenchmarkSlice(set, get, store), ...createDatasetSlice(set, get, store), // Add here ...createRunSlice(set, get, store), });
// src/store/eval/initialState.ts import { datasetInitialState, type DatasetSliceState } from './slices/dataset/initialState'; export interface EvalStoreState extends BenchmarkSliceState, DatasetSliceState { // ... } export const initialState: EvalStoreState = { ...benchmarkInitialState, ...datasetInitialState, // Add here ...runInitialState, };
Step 4: Create Selectors (Optional but Recommended)
// src/store/eval/slices/dataset/selectors.ts import type { EvalStore } from '@/store/eval/store'; export const datasetSelectors = { getDatasetData: (benchmarkId: string) => (s: EvalStore) => s.datasetMap[benchmarkId], getDatasets: (benchmarkId: string) => (s: EvalStore) => s.datasetMap[benchmarkId]?.items ?? [], isLoadingDataset: (id: string) => (s: EvalStore) => s.loadingDatasetIds.includes(id), };
Step 5: Use in Component
// Component - List with Map import { useEvalStore } from '@/store/eval'; import { datasetSelectors } from '@/store/eval/selectors'; const DatasetList = ({ benchmarkId }: { benchmarkId: string }) => { const useFetchDatasets = useEvalStore((s) => s.useFetchDatasets); const datasets = useEvalStore(datasetSelectors.getDatasets(benchmarkId)); const datasetData = useEvalStore(datasetSelectors.getDatasetData(benchmarkId)); useFetchDatasets(benchmarkId); if (datasetData?.isLoading) return <Loading />; return ( <div> <h2>Total: {datasetData?.total ?? 0}</h2> <List data={datasets} /> </div> ); }; // Component - Single item (for modal) const DatasetImportModal = ({ open, datasetId }: Props) => { const useFetchDatasetDetail = useEvalStore((s) => s.useFetchDatasetDetail); const dataset = useEvalStore((s) => s.datasetDetail); const isLoading = useEvalStore((s) => s.isLoadingDatasetDetail); // Only fetch when modal is open useFetchDatasetDetail(open && datasetId ? datasetId : undefined); return ( <Modal open={open}> {isLoading ? <Loading /> : <div>{dataset?.name}</div>} </Modal> ); };
Common Patterns
Pattern 1: List + Detail
// List with pagination useFetchTestCases: (params) => { const { datasetId, limit, offset } = params; return useClientDataSWR( datasetId ? [FETCH_TEST_CASES_KEY, datasetId, limit, offset] : null, () => agentEvalService.listTestCases({ datasetId, limit, offset }), { onSuccess: (data: any) => { set( { testCaseList: data.data, testCaseTotal: data.total, isLoadingTestCases: false, }, false, 'useFetchTestCases/success', ); }, }, ); };
Pattern 2: Dependent Fetching
// Component const BenchmarkDetail = () => { const { benchmarkId } = useParams(); const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail); const benchmark = useEvalStore((s) => s.benchmarkDetail); const useFetchDatasets = useEvalStore((s) => s.useFetchDatasets); const datasets = useEvalStore((s) => s.datasetList); // Fetch benchmark first useFetchBenchmarkDetail(benchmarkId); // Then fetch datasets for this benchmark useFetchDatasets(benchmarkId); return <div>...</div>; };
Pattern 3: Conditional Fetching
// Only fetch when modal is open const DatasetImportModal = ({ open, datasetId }: Props) => { const useFetchDatasetDetail = useEvalStore((s) => s.useFetchDatasetDetail); const dataset = useEvalStore((s) => s.datasetDetail); // Only fetch when open AND datasetId exists useFetchDatasetDetail(open && datasetId ? datasetId : undefined); return <Modal open={open}>...</Modal>; };
Pattern 4: Refresh After Mutation
// Store action createDataset: async (params) => { const result = await agentEvalService.createDataset(params); // Refresh the list after creation await get().refreshDatasets(params.benchmarkId); return result; }; deleteDataset: async (id, benchmarkId) => { await agentEvalService.deleteDataset(id); // Refresh the list after deletion await get().refreshDatasets(benchmarkId); };
Migration Guide: useEffect → Store SWR
Before (❌ Wrong)
const TestCaseList = ({ datasetId }: Props) => { const [data, setData] = useState<any[]>([]); const [loading, setLoading] = useState(false); useEffect(() => { const fetchData = async () => { setLoading(true); try { const result = await lambdaClient.agentEval.listTestCases.query({ datasetId, }); setData(result.data); } finally { setLoading(false); } }; fetchData(); }, [datasetId]); return <Table data={data} loading={loading} />; };
After (✅ Correct)
// 1. Create service method class AgentEvalService { async listTestCases(params: { datasetId: string }) { return lambdaClient.agentEval.listTestCases.query(params); } } // 2. Create store slice export const createTestCaseSlice: StateCreator<...> = (set) => ({ useFetchTestCases: (params) => { return useClientDataSWR( params.datasetId ? [FETCH_TEST_CASES_KEY, params.datasetId] : null, () => agentEvalService.listTestCases(params), { onSuccess: (data: any) => { set( { testCaseList: data.data, isLoadingTestCases: false }, false, 'useFetchTestCases/success', ); }, }, ); }, }); // 3. Use in component const TestCaseList = ({ datasetId }: Props) => { const useFetchTestCases = useEvalStore((s) => s.useFetchTestCases); const data = useEvalStore((s) => s.testCaseList); const loading = useEvalStore((s) => s.isLoadingTestCases); useFetchTestCases({ datasetId }); return <Table data={data} loading={loading} />; };
Best Practices
✅ DO
- Always use service layer - Never call lambdaClient directly in stores/components
- Use SWR hooks in stores - Not useEffect in components
- Clear naming -
for hooks,useFetchXxx
for cache invalidationrefreshXxx - 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
after mutation?refreshXxx() - Is the cache key the same in both hook and refresh?
- Check devtools for state updates
Problem: Loading state stuck
Check:
- Is
updatingonSuccess
?isLoadingXxx: 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
See
skill for detailed patternsstore-data-structures
- Define types in
:@lobechat/types- Detail type (e.g.,
)AgentEvalBenchmark - List item type (e.g.,
)AgentEvalBenchmarkListItem
- Detail type (e.g.,
- Design state structure:
- List:
xxxList: XxxListItem[] - Detail:
xxxDetailMap: Record<string, Xxx> - Loading:
loadingXxxDetailIds: string[]
- List:
- Create reducer if optimistic updates needed
Step 2: Service Layer
- Create service in
src/services/xxxService.ts - Add methods:
-
- fetch listlistXxx() -
- fetch detailgetXxx(id) -
,createXxx()
,updateXxx()
- mutationsdeleteXxx()
-
Step 3: Store Actions
- Create
with state structureinitialState.ts - Create
with:action.ts-
- list SWR hookuseFetchXxxList() -
- detail SWR hookuseFetchXxxDetail(id) -
,refreshXxxList()
- cache invalidationrefreshXxxDetail(id) - CRUD methods calling service
-
andinternal_dispatch
if using reducerinternal_updateLoading
-
- Create
(optional but recommended)selectors.ts - Integrate slice into main store
Step 4: Component Usage
- Use store hooks (NOT useEffect)
- List pages: access
arrayxxxList - 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
skill)store-data-structures - 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
- How to structure List and Detail data in storesstore-data-structures
- General Zustand patterns and best practiceszustand