关于 data-fetching-architecture
面向 LobeHub 开发者的数据获取架构技能,说明如何实现或迁移应用中的数据请求。内容涵盖服务层、Store、SWR、缓存、变更操作及界面状态约定。
主要功能
- 01服务层与 Store 的职责边界
- 02SWR Hook 与缓存键约定
- 03变更操作和乐观更新指南
- 04加载、错误与空状态处理
- 05现有请求迁移清单
适用场景
- 01添加由 Store 管理的读取 Hook
- 02写入后刷新相关缓存
- 03处理列表请求的失败与重试
- 04将组件级请求迁入服务层和 Store
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ lobehub/ lobehub --skill data-fetching-architecture安装后在 Claude Code 中输入/data-fetching-architecture确认已加载。
附带文件references/home-first-paint.mdreferences/walkthrough.md
详细介绍
LobeHub 数据获取架构
Component → Store useFetchXxx hook → Service → lambdaClient
← SWR request state + store data ← response
层级边界
src/services/中的服务负责 API 调用:读取使用.query(),写入使用.mutate()。 每个领域导出一个服务实例;组件和 store 不直接调用lambdaClient。- Store 读取钩子使用
useClientDataSWR,并返回其 SWR 响应,包括error和mutate。通过封装器支持的回调(onSuccess,或同步封装器中的onData), 将成功结果同步到 store。 - 从本地副本渲染的服务端列表和详情应改用
@/libs/replica: store 的useFetchXxx返回slice.useSync(params)(仅标志位),由引擎写入 store。 请参阅 zustand 中的 Replica 部分。 - 组件调用这些钩子,并通过选择器读取 store 数据。不要在
useEffect中获取数据,也不要在组件的useState中重复存储服务端数据。 - 读取钩子使用
useFetchXxx,缓存失效使用refreshXxx。 - 列表/详情类型、映射表、reducer 和共享类型来源请参阅 Zustand 数据结构。有关 store 布局 和 action 类,请参阅 zustand。
首页首次绘制与持久化缓存
更改首页/侧边栏的首次绘制或持久化显示数据时,请阅读
references/home-first-paint.md。其中介绍了如何
避免闪烁、复用持久化数据,以及用户 displaySnapshot.ts 边界。
缓存键与刷新
复用 src/libs/swr/keys.ts 中的领域键工厂;如果该领域没有键,则定义一个共享键。
包含所有会影响响应的参数:实体或父级 ID、筛选条件、排序和分页。读取和刷新必须
构造相同的键。
从 @/libs/swr 导入 useClientDataSWR 和 mutate,以保留应用的
workspace/缓存处理逻辑。缺少必需 ID 时返回 null 键;将 undefined
传给 store 钩子,只有当该钩子将其映射为 null 时才会禁用数据获取。
例如,在 action 类中:
useFetchBenchmarks = () =>
useClientDataSWR(evalKeys.benchmarks(), () => agentEvalService.listBenchmarks(), {
onSuccess: (data) => {
this.#set({ benchmarkList: data, benchmarkListInit: true });
},
});
refreshBenchmarks = async () => {
await mutate(evalKeys.benchmarks());
};
请查看当前的 benchmark action 了解 store 的接线方式,并查看 SWR 封装器 了解其 实际选项。这些链接用于定位实现,并不意味着所有现有调用点都可以原样照搬。
共享已知父级 ID 的钩子可以并行运行。如果某个请求需要另一个请求返回的值,
则在该值可用之前,将其键保持为 null。不要假设应用封装器使用上游 SWR 的默认
去重间隔。
对于按多个父级分别缓存的列表,请参阅 按父级键控的列表。普通的扁平列表无需额外一层。
变更
- 调用服务,然后刷新受影响的列表/详情键,以及响应发生变化的任何相关领域。 如果后续 UI 行为依赖刷新结果,则应等待刷新完成。
- 在 store 状态中保留待处理标志,并在
finally中清除。行更新/删除使用按 ID 区分的状态,以免影响其他行的使用;创建操作可以使用单独的标志,因为此时还没有 持久化 ID。 - 对 replica 数据进行乐观写入时,使用
slice.optimistic/entity.optimistic; 调用被拒绝时会自动回滚,包括删除操作。 - 对旧版 store 执行乐观创建/更新时,使用 store 的 reducer/dispatch 约定;如果服务调用失败,则恢复或重新验证受影响的状态。调用被拒绝后,不要 留下临时行或看似成功的编辑。在这类 store 中,只有服务端成功后才能删除: 手写 reducer 没有回滚基准状态。
- 让错误传递给调用方的错误 UI;
finally块会清除待处理状态,但本身不会恢复 乐观写入。
渲染加载状态、错误和已完成的数据
使用读取钩子的 SWR 响应。仅表示成功的标志(例如 isInit)、缺失的映射项以及 data ?? [],无法区分初始请求失败、加载中和空结果。
标准的加载、错误、空和数据界面请使用 AsyncBoundary;自定义布局和行内错误请使用 AsyncError。将原始 SWR data 传给边界:undefined 表示没有成功结果,而 [] 表示请求已完成且结果为空。
Replica useSync 不会返回 data。将 error 和来自 store 条目的已完成信号传给 AsyncBoundary(data={isListInit || undefined},其中选择器为 entry !== undefined);参见 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>
);
};
示例中的 EmptyState 和 BenchmarkCards 代表该界面现有的渲染器。调整示例时请遵循以下区别:
- 在空状态、
NotFound或零值默认内容之前显示首次加载错误。不要在if (!map[id]) return <Skeleton />之后放置错误分支;失败时映射项可能永远不会填充。 - 后台重新验证失败时保留已完成的内容,包括成功加载的空列表。重试进行中时应显示待处理反馈。
- 对于由获取数据和静态数据组成的列表,请在合并行之前检查获取部分的请求状态。静态行不得掩盖失败的请求。
- 对于无限滚动,请按桶保存
loadMoreError,并显示行内重试行。该错误尚未解决时,应暂停由观察器触发的重试。 - 已关闭的模态框或不存在的 id 会禁用请求;这并不能证明记录不存在。
迁移现有的获取逻辑
将 API 调用移至其 service,并将请求移至 store SWR 钩子。除非任务需要更改,否则沿用现有的状态形状和 action 组织方式。用该钩子和选择器替换组件的 effect/本地获取状态,然后接入 error、重试和待处理反馈。写入后刷新相同的缓存键。
针对正在修改的界面,检查初始失败、重试、已完成的空结果、后台失败以及切换 id 的情况。对于过期数据,请比较读取/刷新键及其更新的 store 桶;对于卡在加载状态的情况,除成功回调外,也要检查被拒绝的请求。不要添加第二次获取或另一个加载标志来掩盖问题。