| 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-structures skill 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-structures skill
Note: For data structure patterns (Map vs Array, List vs Detail), see the store-data-structures skill.
Layer 1: Service Layer
Purpose
- Encapsulate all API calls to lambdaClient
- Provide clean, typed interfaces
- Single source of truth for API operations
Service Structure
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();
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 store-data-structures skill for how to structure List and Detail data.
Store Structure Overview
import type { AgentEvalBenchmark, AgentEvalBenchmarkListItem } from '@lobechat/types';
export interface BenchmarkSliceState {
benchmarkList: AgentEvalBenchmarkListItem[];
benchmarkListInit: boolean;
benchmarkDetailMap: Record<string, AgentEvalBenchmark>;
loadingBenchmarkDetailIds: string[];
isCreatingBenchmark: boolean;
isUpdatingBenchmark: boolean;
isDeletingBenchmark: boolean;
}
For complete initialState, reducer, and internal dispatch patterns, see the store-data-structures skill.
Create Actions
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 {
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>;
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',
);
},
});
},
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;
get().internal_dispatchBenchmarkDetail({
type: 'updateBenchmarkDetail',
id,
value: params,
});
get().internal_updateBenchmarkDetailLoading(id, true);
try {
await agentEvalService.updateBenchmark(params);
await get().refreshBenchmarks();
await get().refreshBenchmarkDetail(id);
} finally {
get().internal_updateBenchmarkDetailLoading(id, false);
}
},
deleteBenchmark: async (id) => {
get().internal_dispatchBenchmarkDetail({
type: 'deleteBenchmarkDetail',
id,
});
get().internal_updateBenchmarkDetailLoading(id, true);
try {
await agentEvalService.deleteBenchmark(id);
await get().refreshBenchmarks();
} finally {
get().internal_updateBenchmarkDetailLoading(id, false);
}
},
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',
);
},
});
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:
import { useEvalStore } from '@/store/eval';
const BenchmarkList = () => {
const useFetchBenchmarks = useEvalStore((s) => s.useFetchBenchmarks);
const benchmarks = useEvalStore((s) => s.benchmarkList);
const isInit = useEvalStore((s) => s.benchmarkListInit);
useFetchBenchmarks();
if (!isInit) return <Loading />;
return (
<div>
<h2>Total: {benchmarks.length}</h2>
{benchmarks.map(b => <BenchmarkCard key={b.id} {...b} />)}
</div>
);
};
Fetching Detail Data:
import { useEvalStore } from '@/store/eval';
import { useParams } from 'react-router-dom';
const BenchmarkDetail = () => {
const { benchmarkId } = useParams<{ benchmarkId: string }>();
const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail);
const benchmark = useEvalStore((s) =>
benchmarkId ? s.benchmarkDetailMap[benchmarkId] : undefined,
);
const isLoading = useEvalStore((s) =>
benchmarkId ? s.loadingBenchmarkDetailIds.includes(benchmarkId) : false,
);
useFetchBenchmarkDetail(benchmarkId);
if (!benchmark) return <Loading />;
return (
<div>
<h1>{benchmark.name}</h1>
<p>{benchmark.description}</p>
{isLoading && <Spinner />}
</div>
);
};
Using Selectors (Recommended):
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>;
};
What NOT to Do
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
import { useEvalStore } from '@/store/eval';
import { benchmarkSelectors } from '@/store/eval/selectors';
const CreateBenchmarkModal = () => {
const createBenchmark = useEvalStore((s) => s.createBenchmark);
const handleSubmit = async (values) => {
try {
await createBenchmark(values);
message.success('Created successfully');
onClose();
} catch (error) {
message.error('Failed to create');
}
};
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}>Update</button>
<button onClick={handleDelete}>Delete</button>
</div>
);
};
Data Structures: For detailed comparison of List vs Detail patterns, see the store-data-structures skill.
Complete Example: Adding a New Feature
Scenario: Add "Dataset" data fetching with optimistic updates
Step 1: Create Service
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);
}
}
Step 2: Create Reducer
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
import type { Dataset } from '@/types/dataset';
export interface DatasetData {
currentPage: number;
hasMore: boolean;
isLoading: boolean;
items: Dataset[];
pageSize: number;
total: number;
}
export interface DatasetSliceState {
datasetMap: Record<string, DatasetData>;
datasetDetail: Dataset | null;
isLoadingDatasetDetail: boolean;
loadingDatasetIds: string[];
}
export const datasetInitialState: DatasetSliceState = {
datasetMap: {},
datasetDetail: null,
isLoadingDatasetDetail: false,
loadingDatasetIds: [],
};
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 {
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);
}
},
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',
);
},
});
Step 3: Integrate into Store
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),
});
import { datasetInitialState, type DatasetSliceState } from './slices/dataset/initialState';
export interface EvalStoreState extends BenchmarkSliceState, DatasetSliceState {
}
export const initialState: EvalStoreState = {
...benchmarkInitialState,
...datasetInitialState,
...runInitialState,
};
Step 4: Create Selectors (Optional but Recommended)
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
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>
);
};
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>
);
};
Common Patterns
Pattern 1: List + Detail
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
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>;
};
Pattern 3: Conditional Fetching
const DatasetImportModal = ({ open, datasetId }: Props) => {
const useFetchDatasetDetail = useEvalStore((s) => s.useFetchDatasetDetail);
const dataset = useEvalStore((s) => s.datasetDetail);
useFetchDatasetDetail(open && datasetId ? datasetId : undefined);
return <Modal open={open}>...</Modal>;
};
Pattern 4: Refresh After Mutation
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);
};
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)
class AgentEvalService {
async listTestCases(params: { datasetId: string }) {
return lambdaClient.agentEval.listTestCases.query(params);
}
}
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',
);
},
},
);
},
});
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 -
useFetchXxx for hooks, refreshXxx for 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
onSuccess updating 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 store-data-structures skill for detailed patterns
Step 2: Service Layer
Step 3: Store Actions
Step 4: Component Usage
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-structures skill)
- 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