data-fetching-architecture
GitHub定义前端数据获取架构,规范从组件到 Service 的调用链路,强制使用 SWR 钩子、Zustand 状态管理及缓存失效策略,优化首屏渲染与持久化缓存。
Trigger Scenarios
Install
npx skills add lobehub/lobehub --skill data-fetching-architecture -g -y
SKILL.md
Frontmatter
{
"name": "data-fetching-architecture",
"description": "Use for client APIs, SWR hooks, cache invalidation, async errors, useEffect migration, home first paint and persistent caches.",
"user-invocable": false
}
LobeHub Data Fetching Architecture
Component → Store useFetchXxx hook → Service → lambdaClient
← SWR request state + store data ← response
Layer boundaries
- Services in
src/services/own API calls:.query()for reads,.mutate()for writes. Export a service instance per domain; components and stores do not calllambdaClientdirectly. - Store read hooks use
useClientDataSWRand return its SWR response, includingerrorandmutate. Sync successful results into the store through the wrapper's supported callback (onSuccess, oronDatafor the sync wrapper). - Server lists and details that paint from a local copy go through
@/libs/replicainstead: the store'suseFetchXxxreturnsslice.useSync(params)(flags only) and the engine writes the store. See the Replica section of zustand. - Components call these hooks and read store data through selectors. Do not fetch in
useEffector duplicate server data in componentuseState. - Use
useFetchXxxfor read hooks andrefreshXxxfor cache invalidation. - For list/detail types, maps, reducers, and shared type sources, use Zustand data structures. For store layout and action classes, use zustand.
Home First Paint and Persistent Caches
When changing home/sidebar first paint or persisted display data, read
references/home-first-paint.md. It covers
avoiding flicker, reusing persistence, and the user displaySnapshot.ts boundary.
Cache keys and refresh
Reuse the domain key factory in src/libs/swr/keys.ts; define a shared key if the
domain has none. Include every parameter that changes the response: entity or parent
id, filters, sort, and pagination. Read and refresh must construct the same key.
Import useClientDataSWR and mutate from @/libs/swr, keeping the application's
workspace/cache handling. Return a null key when a required id is absent; passing
undefined to a store hook only disables fetching if that hook maps it to null.
For example, inside an action class:
useFetchBenchmarks = () =>
useClientDataSWR(evalKeys.benchmarks(), () => agentEvalService.listBenchmarks(), {
onSuccess: (data) => {
this.#set({ benchmarkList: data, benchmarkListInit: true });
},
});
refreshBenchmarks = async () => {
await mutate(evalKeys.benchmarks());
};
Read the current benchmark action for store wiring and the SWR wrappers for their actual options. These links locate implementation; they do not make every existing call site a template to copy unchanged.
Hooks that share a known parent id can run together. If one request needs a value
returned by another, keep its key null until that value exists. Do not assume the
application wrapper uses upstream SWR's default deduplication interval.
For lists cached separately under multiple parents, see parent-keyed lists. Ordinary flat lists need no extra layer.
Mutations
- Call the service, then refresh affected list/detail keys and any related domain whose response changed. Await refresh when subsequent UI behavior depends on it.
- Keep pending flags in store state and clear them in
finally. Use per-id state for row updates/deletes so unrelated rows remain usable; create can use a separate flag because no persistent id exists yet. - For optimistic writes on replica data, use
slice.optimistic/entity.optimistic; a rejected call rolls back on its own, deletes included. - For optimistic create/update on legacy stores, use the store's reducer/dispatch convention and restore or revalidate affected state if the service fails. Do not leave a temporary row or a successful-looking edit after rejection. Delete only after server success there: a hand-written reducer has no rollback base.
- Let failures reach the caller's error UI; a
finallyblock clears pending state but does not by itself recover an optimistic write.
Render loading, errors, and settled data
Consume the read hook's SWR response. Success-only flags such as isInit, missing
map entries, and data ?? [] cannot distinguish an initial request failure from
loading or empty results.
Use AsyncBoundary for standard loading/error/empty/data surfaces; use AsyncError
for custom layouts and inline failures. Pass the original SWR data to the boundary:
undefined means no successful result, whereas [] is a settled empty result.
Replica useSync returns no data. Feed AsyncBoundary its error and a settled
signal from the store entry (data={isListInit || undefined}, where the selector is
entry !== undefined); see src/features/Pages/PageLayout/Body/index.tsx.
const BenchmarkList = () => {
const useFetchBenchmarks = useEvalStore((s) => s.useFetchBenchmarks);
const benchmarks = useEvalStore((s) => s.benchmarkList);
const { data, error, isLoading, mutate } = useFetchBenchmarks();
return (
<AsyncBoundary
data={data}
empty={<EmptyState />}
error={error}
isEmpty={data?.length === 0}
isLoading={isLoading}
onRetry={() => {
void mutate();
}}
>
<BenchmarkCards items={benchmarks} />
</AsyncBoundary>
);
};
The example's EmptyState and BenchmarkCards stand for the surface's existing
renderers. Follow these distinctions when adapting it:
- Show first-load errors before empty /
NotFound/ zero-value defaults. Do not put an error branch afterif (!map[id]) return <Skeleton />; failure may never fill the map. - Preserve settled content during background revalidation failures, including a successfully loaded empty list. A retry in flight should show pending feedback.
- In a fetched + static list, use the fetched slice's request state before combining rows. Static rows must not hide a failed request.
- For infinite scroll, keep a per-bucket
loadMoreErrorand show an inline Retry row. Suspend observer-triggered retries while that error is unresolved. - A closed modal or absent id disables a request; it is not evidence of a missing record.
Migrating an existing fetch
Move the API call into its service and the request into a store SWR hook. Reuse the
existing state shape and action organization unless they need to change for the task.
Replace the component's effect/local fetch state with that hook and selectors, then
connect error, retry, and pending feedback. Refresh the same cache key after writes.
Check initial failure, retry, settled-empty results, background failure, and switching ids against the surface being changed. For stale data, compare read/refresh keys and the store bucket they update; for stuck loading, inspect rejected requests as well as success callbacks. Do not add a second fetch or another loading flag to mask the cause.
Version History
-
aace12f
Current 2026-10-20 06:04
新增 Replica 引擎集成说明,将部分列表/详情数据迁移至本地副本引擎;细化 Store 中 Action 类的职责分离及类型导入修复。
- e0d4b61 2026-09-23 09:51
- 29fe043 2026-08-20 18:33


