ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Cherry Studio 渲染进程 DataApi 完全指南:类型安全的 React 数据请求体系

Cherry Studio 渲染进程 DataApi 完全指南:类型安全的 React 数据请求体系 Cherry Studio 渲染进程 DataApi 完全指南类型安全的 React 数据请求体系【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio本篇指南以 Cherry Studio 开源仓库的渲染进程数据层为对象系统讲解 DataApi 系统在 React 组件中的完整使用方式从useQuery、useMutation、useInfiniteQuery、usePaginatedQuery四大 Hook到动态路径、refresh缓存失效模式、跨窗口数据变更通知、DataApiService直连与错误处理并辅以仓库源码与测试用例作为实现依据。读完本文你将能在 Cherry Studio 的 Electron 渲染进程中以端到端类型安全的方式编写可缓存、可重试、可跨窗口收敛的业务数据请求代码。背景什么是渲染进程的 DataApiDataApi 是 Cherry Studio 中面向 SQLite 业务数据的类型安全 IPC 通信体系。在渲染进程一侧DataApiServicesrc/renderer/data/DataApiService.ts扮演 API 客户端/网关角色它向上层 React 组件提供 RESTful 风格的get/post/put/patch/delete接口内部完成请求序列化、基于指数退避的自动重试、3 秒默认超时以及错误归一化再通过 preload 桥接的window.api.dataApi.request走 IPC 到达主进程的 IpcAdapter → ApiServer → Handler → Service → SQLite 链路。在此基础上src/renderer/data/hooks/useDataApi.ts 基于 SWR 封装了一组 React Hook把缓存、去重、失效、乐观更新、分页等复杂性收敛到组件外部。该模块的 JSDoc 明确列出了全部导出useQuery、useMutation、useInfiniteQuery、usePaginatedQuery、useDataChange、useInvalidateCache、useReadCache、useWriteCache与prefetch。由于 DataApi 走 IPC 而非 HTTP且DataApiService已通过DataApiError.isRetryable实现单层重试Hook 层的默认 SWR 配置刻意关闭了 HTTP 风格的能力useDataApi.ts 中DEFAULT_SWR_OPTIONSconst DEFAULT_SWR_OPTIONS { revalidateOnFocus: false, // 焦点事件不意味着数据过期 revalidateOnReconnect: false, // IPC 没有“重连”语义 dedupingInterval: 5000, // 5 秒内去重重复请求 shouldRetryOnError: false, // 重试决策唯一交给 DataApiService keepPreviousData: true // 新 key 拉取期间保留旧数据避免搜索/翻页闪烁 } as const理解这组默认值是理解后续所有 Hook 行为的前提组件通过isRefreshing后台校验与isLoading无缓存的首屏加载的区分来感知数据新鲜度。React Hooks 概览与选型useQueryGET 请求useQuery负责带缓存与自动校验的读取。它的基本形态、查询参数、路径参数推断、条件请求与手动刷新如下源码签名见 useDataApi.ts 的useQuery定义import { useQuery } from data/hooks/useDataApi // 基本用法 const { data, isLoading, error } useQuery(/topics) // 带查询参数 const { data: messages } useQuery(/messages, { query: { topicId: abc123, page: 1, limit: 20 } }) // 路径参数从路径自动推断如 /topics/abc123 返回 Topic const { data: topic } useQuery(/topics/abc123) // 条件请求依赖未就绪时跳过 const { data } useQuery(/topics, { enabled: !!topicId }) // 手动刷新 const { data, mutate, refetch } useQuery(/topics) refetch() // 或 await mutate()useQuery的返回值类型定义在 UseQueryResultdata、isLoading首屏加载、isRefreshing后台校验、error、refetch与mutateSWR 原生 mutator可做乐观更新或手动缓存操作。enabled: false时 Hook 将解析后的 key 置为nullSWR 即停止请求——这是依赖查询的标准做法。useMutationPOST / PUT / PATCH / DELETEuseMutation承载写操作并暴露加载态import { useMutation } from data/hooks/useDataApi // 创建POST const { trigger: createTopic, isLoading } useMutation(POST, /topics) const newTopic await createTopic({ body: { name: New Topic } }) // 全量替换PUT const { trigger: replaceTopic } useMutation(PUT, /topics/abc123) await replaceTopic({ body: { name: Updated Name, description: ... } }) // 局部更新PATCH const { trigger: updateTopic } useMutation(PATCH, /topics/abc123) await updateTopic({ body: { name: New Name } }) // 删除 const { trigger: deleteTopic } useMutation(DELETE, /topics/abc123) await deleteTopic() // 成功后自动刷新其他查询 const { trigger } useMutation(POST, /topics, { refresh: [/topics], // 成功后失效这些 key onSuccess: (data) logger.info(Created:, data) })关于函数返回值的身份稳定性这是该层的一条官方契约trigger、invalidate、refetch、nextPage、prevPage、reset等在重渲染之间保持稳定身份与 SWR 自身的mutate/trigger一致仅当有意义的输入变化时如nextPage在翻页可用性翻转时才改变。useMutation的trigger通过 ref 读取其 options源码中optionsRefuseEffect同步因此内联的 options 对象永远不会搅动trigger的身份——这使你可以放心地把trigger直接放进useCallback/useEffect的依赖数组。源码还给出了成功的副作用执行顺序见 useMutation 的 remarks服务端响应 resolverefreshkey 被失效——覆盖useQuery、usePaginatedQuery与useInfiniteQuery/useSWRInfiniteinfinite 缓存会被显式枚举因为 SWR 的 filter API 会跳过$inf$前缀的 keyonSuccess回调执行。此时回调里触碰的useQuery处于 “stale、pending revalidation” 状态——应避免在此处手动乐观mutate(...)以免与待执行的校验竞争若设置了optimisticData被写入的缓存 key 会被重新校验。refresh回调若抛出异常会被捕获并记录日志不会导致trigger的 Promise 拒绝也不会跳过onSuccess。useInfiniteQuery基于游标Cursor的无限滚动useInfiniteQuery面向 “加载更多” 的无限滚动 UI。与常见的“自动拼接扁平数组”不同该 Hook 暴露的是原始响应数组pages由消费者用useInfiniteFlatItems派生出扁平列表并显式选择与端点分页形状、容器布局相匹配的顺序import { useInfiniteQuery, useInfiniteFlatItems } from data/hooks/useDataApi // 简单 feed第 0 页最新、页内降序——页序即展示序 const { pages, hasNext, loadNext, isLoading } useInfiniteQuery(/feed) const items useInfiniteFlatItems(pages) // 聊天容器 column-reverse 中的分支遍历第 0 页最新、页内升序。 // reverseItems: true 翻转每一页使扁平输出最新在前直接喂给反转布局。 const { pages, hasNext, loadNext } useInfiniteQuery(/topics/:topicId/messages, { params: { topicId } }) const messages useInfiniteFlatItems(pages, { reverseItems: true }) const activeNodeId pages[0]?.activeNodeId ?? null // 顶层元数据无需类型断言 // 非 column-reverse 容器中的时间升序渲染翻转页序 const items useInfiniteFlatItems(pages, { reversePages: true })useInfiniteFlatItems提供两个相互独立的开关源码见 useInfiniteFlatItemsreversePages在扁平化前翻转页序reverseItems在扁平化前翻转页内条目。其输出引用在pages与开关稳定时保持稳定内部为useMemo配合useInfiniteQuery稳定化的pagesuseMemo包裹swrResult.data可避免下游重渲染。实现层面的关键点源码依据 useInfiniteQuery编译期约束路径泛型经由CursorPaginatedPath限制——offset 分页的路径在编译期被拒绝getKey以previousPageData.nextCursor是否缺失作为终止条件cursor/limit由 Hook 内部管理limit默认 10pages在 SWR 底层数据未变时跨重渲染保持引用稳定hasNext由最后一页是否存在nextCursor判定loadNext通过setSize(s s 1)加载下一页快速双击由 SWR 的dedupingInterval去重顶级响应元数据如BranchMessagesResponse的activeNodeId/rootId/assistantId以完整精度保留在pages[0]上无需类型断言。usePaginatedQuery基于偏移Offset的分页导航usePaginatedQuery面向带上一页/下一页控件的页式导航同样在编译期拒绝游标分页路径import { usePaginatedQuery } from data/hooks/useDataApi const { items, page, total, hasNext, hasPrev, nextPage, prevPage } usePaginatedQuery(/topics, { limit: 10 }) // items: 当前页条目只读——排序/修改前请先复制 // page/total: 当前页码1 基与总数 // nextPage()/prevPage(): 翻页实现要点源码见 usePaginatedQuery内部用useState(1)管理currentPage通过unstable_serialize计算查询 key当查询内容变化时自动重置回第 1 页——key 重排如{a,b}vs{b,a}不会触发误重置page/limit由 Hook 内部追加进 querylimit默认 10nextPage/prevPage以hasNext/hasPrev为门控并做 useCallback 记忆化只在翻页可用性真正翻转时改变身份items在数据未就绪时指向冻结的空数组常量EMPTY_ITEMSObject.freeze([])避免空态下items身份抖动完整返回还包括isLoading、isRefreshing、error、refresh、reset。分页 Hook 选型使用场景选用 Hook无限滚动、聊天、feeduseInfiniteQuery页式导航、表格usePaginatedQuery手动控制useQuery每个分页 Hook 都把其路径泛型约束到对应的分页形态把游标路径传给usePaginatedQuery、或把 offset 路径传给useInfiniteQuery是编译期错误而非静默的运行时挂起。其类型机制位于 CursorPaginatedPath / OffsetPaginatedPath二者基于InferPaginationMode判别——先检查 offset 形态以打破可选nextCursor字段造成的结构兼容歧义当路径不在ApiSchemas中导致ResponseForPath回退为any时InferPaginationModeany为never该守卫同样拒绝此路径。因此使用分页 Hook 时务必让 TypeScript 从路径字面量推断TPath显式注入泛型可能绕过守卫。完整的偏移 vs 游标分页模型何时选择哪种、线上契约、服务端实现见 数据分页指南。其核心结论一个端点要么是 offset 要么是 cursor在 schema 中一次性声明、不可由调用方配置——offset 用于需要精确total的页式 UI助手、MCP 服务器cursorkeyset用于无界增长或新数据写入频繁的列表消息、会话、翻译/绘画历史cursor 响应也可额外携带total知识库、文件。动态路径具体路径 vs 模板路径Hook 接受两种路径形式具体路径id 已内联如/providers/abc123或模板路径带:placeholders配合独立params选项// 具体路径——当 id 在调用方稳定时使用props、hook 参数等 const { data } useQuery(/providers/${providerId}) const { data } useQuery(providerPath(providerId)) // 等价字符串的辅助函数 // 模板路径——当同一个 hook 实例要随时间操作不同 id 时使用 // 侧边栏列表、命令面板、URL 处理器、循环内的行级操作 const { data } useQuery(/providers/:providerId, { params: { providerId } }) const { trigger } useMutation(DELETE, /providers/:providerId/api-keys/:keyId, { refresh: ({ args }) [ /providers/${args.params.providerId}, /providers/${args.params.providerId}/api-keys ] }) await trigger({ params: { providerId, keyId } })两种形式产生逐字节完全一致的 SWR 缓存 key因此用一种形式读取、用另一种形式刷新依然保持一致。这由两条实现保证resolveTemplatesrc/renderer/data/utils/dataApiPath.ts把模板渲染成具体路径贪婪占位符:name*允许值内含/前导斜杠锚定使models:resolve这类动词风格后缀不被破坏缺少必需占位符时抛Missing param ...。buildSWRKeyuseDataApi.ts 内部工具在 query 非空时生成[path, query]元组空时生成[path]。该不变量在 useDataApi.test.ts 的buildSWRKey cache-key equivalence用例 中有直接断言resolveTemplate(/providers/:providerId, { providerId: abc })与字面量/providers/abc产生相等的 key——测试注释明确指出“这里的漂移会导致极难排查的幽灵刷新漏失”。何时用哪种形式场景形式ProviderSettings providerId{id}props 传入稳定 id具体路径侧边栏 “删除任意 provider” 操作模板路径命令面板 / URL 处理器操作任意 id模板路径.map()内的行操作——每行一个 hook具体路径注意模板useMutation上的并发 triggeruseSWRMutation以路径为 key 记录isMutating/error状态因此单个模板路径的useMutation实例会跨所有 params 共享加载状态。从同一个 hook 实例并发触发不同 id 会混淆它们的状态// ❌ 错误isMutating 是共享的第二个 trigger 会覆盖第一个 const { trigger, isLoading } useMutation(DELETE, /providers/:providerId) await Promise.all([ trigger({ params: { providerId: a } }), trigger({ params: { providerId: b } }) ]) // ✅ 推荐每行挂载一个 hook绑定具体路径 function ProviderRow({ id }) { const { trigger, isLoading } useMutation(DELETE, providerPath(id)) return button onClick{() trigger()} disabled{isLoading}Delete/button }在开发模式下带变化 params 的并发 trigger 会打印警告。源码实现useMutation 内部用inFlightParamsRef同步记录在飞 params——之所以不用 SWR 的isMutating是因为 React 状态更新落后于渲染Promise.all([trigger(a), trigger(b)])这类同步突发会让两个闭包都读到过期的isMutating false警告将永远不会触发而 ref 在 trigger 入口同步更新。Refresh Patterns三种缓存失效形式refresh声明一次成功的 mutation 之后要失效哪些 SWR 缓存 key。支持三种形式按需选择最精确的一种。静态路径精确匹配useMutation(POST, /topics, { refresh: [/topics] })只失效[/topics]。适用于确切知道受影响的路径、且这些路径不依赖 mutation 的入参或出参的场景。/*后缀前缀匹配// 失效 /providers、/providers/abc、/providers/abc/api-keys、/providers/abc/api-keys/k1…… useMutation(DELETE, /providers/:providerId, { refresh: ({ args }) [/providers, /providers/${args.params.providerId}/*] })前缀末尾的斜杠自动保留防止/providers-archived这类同名兄弟资源的误伤。实现上createKeyMatcher对/*模式切掉*、保留尾部/用key[0].startsWith(prefix)匹配useDataApi.ts 内部工具测试用例明确断言/providers-archived与/providers-archived/xyz都不会命中/providers/*见 createKeyMatcher 测试。/*的独特价值在于失效mutation 不知道 id 的子路径实例——例如在组件树别处订阅的useQuery(/providers/abc/api-keys/keyId-001)条目函数形式的枚举无法命名这些 key。函数形式动态 key// 失效 key 依赖 trigger 入参 useMutation(DELETE, /messages/:messageId, { refresh: ({ args }) [/topics/${args.body.topicId}/tree] }) // 失效 key 依赖服务端响应 useMutation(POST, /messages, { refresh: ({ result }) [/topics/${result.topicId}/messages, /messages/${result.parentId}] })回调上下文类型为 RefreshContextargs本次trigger的入参与resultmutation 的服务端响应。适用于 key 集合只有到调用时刻才知道id 来自 args/result的场景。形式选择需求形式静态、已知 key数组失效某资源的所有子路径数组中的/*前缀失效由 args / result 计算出的 key函数两者兼要扇出 精确返回精确与/*混合的函数需要避免的误用不要把/*当作全缓存重置。[/*]或/m*这类短前缀在开发模式下会抛错。永远写完整的路径段assertValidPattern的强制规则见 useDataApi.ts测试覆盖见 dev-mode pattern assertions。静态数组够用时不要用函数形式。额外运行时开销且掩盖意图。不要对高基数列表使用/*如/messages/*。它会重新校验所有窗口中每个消息级查询。应改用带特定父 id 的函数形式/topics/${id}/messages。同一模块内不要混用模板路径与辅助函数。缓存 key 虽相同但代码评审变难。每个模块只选一种形式。refresh只用于 DataApi 的 key。非 SQLite 数据Cache、Preference有自己的失效机制。值得一提的实现细节由于 SWR 的 filter API 会跳过$inf$前缀的 infinite keyinvalidatePathPatternsuseDataApi.ts 内部工具做了双层扇出——先用createMultiKeyMatcher走 filter 通道处理数组 key再通过findMatchingInfiniteKeysextractInfinitePath显式枚举并逐个mutate无限滚动 key路径从$inf$path,...形态中按未转义引号边界安全解析用户参数含也不受影响。数据变更通知Data Change Notificationsrefresh只覆盖本窗口自己的 mutation。对于其他窗口或主进程后台路径发起的写入主进程在每次提交的写入后广播DataApiDataChangeEffect[]消费者按端点订阅并自行决定收敛方式重新校验 / 重建 / 忽略import { useDataChange } from data/hooks/useDataApi // 保守的列表收敛任何信号 → refetch const { refetch } useQuery(/topics) useDataChange(/topics, () refetch()) // 多端点每个通知合并为一次回调 useDataChange([/topics, /topics/latest], () refreshAll()) // 按 id 表面用 entityIds 过滤缺席 无声明 → 视为相关 useDataChange(/topics/:id, (effects) { if (effects.some((e) !e.entityIds || e.entityIds.includes(myId))) mutate() }) // 非 React 代码同一设施走 service返回取消订阅函数 const unsubscribe dataApiService.onDataChanged(/topics, (effects) { ... })语义由 Phase A 契约冻结具体如下端点精确匹配——没有前缀/通配符订阅effect 由endpoint 可选kindprojection/membership/orderdimensionentityIds构成。一个业务操作 一次回调一个通知内所有匹配条目合并为单次调用送达通知之间不做聚合。端点之下的一切都是消费者策略dimension/entityIds 过滤、收敛选择、以及对自身写入回波的幂等性发起窗口同样会收到自己的信号。提示只做收窄省略dimension/entityIds意味着“无声明——假定相关”绝不意味着“无影响”。尽力送达只送达活跃且持续订阅的渲染进程每窗口 FIFO。消费者订阅注册之前含主进程启动期间已提交的变更不会被信号化恢复手段是端点的下一次变更、重新挂载或任意一次新查询。useDataChange的源码实现src/renderer/data/hooks/useDataChange.ts通过 ref 持有最新 listener 与routeParams按endpoints的稳定 keyjoin(\0)建立订阅传入routeParams时会对 effect 的routeParams做字段级匹配过滤。底层设施是DataApiService.onDataChangedDataApiService.ts它以Mapendpoint, Setlistener管理订阅该 map 是设施的唯一状态每个注册包一层唯一 wrapper 保证同 listener 多次注册相互独立dispatchDataChange按精确端点匹配把同一通知的命中条目合并成单批回调且单消费者抛错被隔离、不阻塞其他监听者。DataApiService 直接使用非 React 代码的入口对非 React 代码或需要更多控制的场景直接使用dataApiService单例src/renderer/data/DataApiService.tsimport { dataApiService } from data/DataApiService // GET 请求 const topics await dataApiService.get(/topics) const topic await dataApiService.get(/topics/abc123) const messages await dataApiService.get(/topics/abc123/messages, { query: { page: 1, limit: 20 } }) // POST 请求 const newTopic await dataApiService.post(/topics, { body: { name: New Topic } }) // PUT 请求全量替换 const updatedTopic await dataApiService.put(/topics/abc123, { body: { name: Updated, description: Full update } }) // PATCH 请求局部更新 const patchedTopic await dataApiService.patch(/topics/abc123, { body: { name: Just update name } }) // DELETE 请求 await dataApiService.delete(/topics/abc123)该服务的重试机制位于sendRequestDataApiService.ts默认重试配置为maxRetries: 2、retryDelay: 1000毫秒、backoffMultiplier: 2可通过configureRetry覆盖重试决策由DataApiError.isRetryable驱动延迟按retryDelay * backoffMultiplier^retryCount指数退避重试会生成新 requestId请求走Promise.race与 3 秒超时竞争超时抛出ErrorCode.TIMEOUT客户端错误4xx不参与重试。错误处理使用 Hookfunction TopicList() { const { data, isLoading, error } useQuery(/topics) if (isLoading) return Loading / if (error) { if (error.code ErrorCode.NOT_FOUND) { return NotFound / } return Error message{error.message} / } return List items{data} / }使用 try-catchimport { DataApiError, ErrorCode } from shared/data/api/errors try { await dataApiService.post(/topics, { body: data }) } catch (error) { if (error instanceof DataApiError) { switch (error.code) { case ErrorCode.VALIDATION_ERROR: // 处理校验错误 const fieldErrors error.details?.fieldErrors break case ErrorCode.NOT_FOUND: // 处理未找到 break case ErrorCode.CONFLICT: // 处理冲突 break default: // 处理其他错误 } } }可重试错误if (error instanceof DataApiError error.isRetryable) { // 可以安全重试SERVICE_UNAVAILABLE、TIMEOUT 等 await retry(operation) }isRetryable是一个基于RETRYABLE_ERROR_CODES集合的 gettersrc/shared/data/api/errors.tsSERVICE_UNAVAILABLE503、TIMEOUT504、RATE_LIMIT_EXCEEDED429、DATABASE_ERROR500、INTERNAL_SERVER_ERROR500与RESOURCE_LOCKED423被视为可能随重试成功的临时失败。DataApiError还具备跨 IPC 的序列化能力toJSON/fromJSON渲染进程从主进程拿回的是反序列化重建的完整错误对象。常见模式创建表单function CreateTopicForm() { // 用 refresh 选项在创建后自动刷新 /topics const { trigger: createTopic, isLoading } useMutation(POST, /topics, { refresh: [/topics] }) const handleSubmit async (data: CreateTopicDto) { try { await createTopic({ body: data }) toast.success(Topic created) } catch (error) { toast.error(Failed to create topic) } } return ( form onSubmit{handleSubmit} {/* form fields */} button disabled{isLoading} {isLoading ? Creating... : Create} /button /form ) }乐观更新function TopicItem({ topic }: { topic: Topic }) { // 用 optimisticData 实现自动乐观更新与失败回滚 const { trigger: updateTopic } useMutation(PATCH, /topics/${topic.id}, { optimisticData: { ...topic, starred: !topic.starred } }) const handleToggleStar async () { try { await updateTopic({ body: { starred: !topic.starred } }) } catch (error) { // 设置 optimisticData 后回滚自动发生 toast.error(Failed to update) } } return ( div span{topic.name}/span button onClick{handleToggleStar} {topic.starred ? ★ : ☆} /button /div ) }乐观更新的实现路径useMutation 内部trigger先以globalMutate([resolvedPath], optimisticData, false)立即写入缓存第三个参数false表示覆盖值且跳过校验成功后对同一 key 再globalMutate重新校验以对齐服务端真相失败则重新校验完成自动回滚。trigger中执行的refresh以闭包捕获本次调用的 args/result因此并发触发不会互相污染刷新上下文。依赖查询function MessageList({ topicId }: { topicId: string }) { // 第一个查询获取 topic const { data: topic } useQuery(/topics/${topicId}) // 第二个查询依赖第一个topic 存在时才执行 const { data: messages } useQuery( topic ? /topics/${topicId}/messages : null ) if (!topic) return Loading / return ( div h1{topic.name}/h1 MessageList messages{messages} / /div ) }轮询更新function LiveTopicList() { const { data } useQuery(/topics, { refreshInterval: 5000 // 每 5 秒轮询一次 }) return List items{data} / }类型安全整个 API 基于 schema 定义完全类型化——类型从ApiSchemas派生客户端调用、主进程 handler 与响应形状全程编译期校验// 类型从 schema 推断 const { data } useQuery(/topics) // data 的类型为 PaginatedResponseTopic const { trigger } useMutation(POST, /topics) // trigger 期望 { body: CreateTopicDto } // 返回 Topic // 路径参数经过类型检查 const { data: topic } useQuery(/topics/abc123) // TypeScript 知道这里返回 Topic路径参数的类型系统由 src/shared/data/api/types.ts 的ConcreteApiPaths与 src/shared/data/api/paths.ts 的BodyForPath/QueryParamsForPath/ResponseForPath/ParamsForPath/TemplateApiPaths支撑。ParamsOptionuseDataApi.ts在类型层区分模板路径params必填与具体路径params禁止TriggerArgs在此基础上叠加可选的body与query。进阶工具缓存读写的单一受控出口除四大核心 Hook 外useDataApi.ts 还提供三个面向缓存操控的官方工具值得在编写复杂交互时优先采用useInvalidateCache手动失效并触发重新校验。支持invalidate(/topics)精确、invalidate([/topics, /providers/*])多模式混合与invalidate(true)全部失效路径形式同时覆盖普通数组 key 与useSWRInfinitekey。useReadCache非响应式快照读取——调用不订阅、不触发重渲染适合在回调/乐观更新 reducer 中做一次性读取。这是代码库中唯一获准触碰 SWR 内部unstable_serialize与原始 cache API 的地方任何其他需要非响应式读缓存的 hook 都必须经由它从而把不稳定表面限制在单文件内。useWriteCache向 GET key 写入值且不触发校验等价于mutate(key, value, false)是 DataApi 层乐观覆盖的规范形式useReorder及未来的乐观覆盖 hook 都经由它而不是直接碰useSWRConfig。prefetch在用户交互前预热缓存例如onMouseEnter{() prefetch(/topics/abc)}模板路径 params 会生成与useQuery完全一致的缓存 key后续useQuery立即命中缓存。最佳实践清单组件优先用 HookuseQuery与useMutation已处理加载/错误状态选对分页 Hook无限滚动用useInfiniteQuery页式导航用usePaginatedQuery用useInfiniteFlatItems派生扁平项按端点分页形态与容器布局显式选择reversePages/reverseItems——永远不要假设“页面加载顺序”等于“条目展示顺序”处理加载状态数据加载期间始终给出反馈区分isLoading与isRefreshing优雅处理错误为用户提供有意义的错误信息mutation 后重新校验用refresh选项保持 UI 同步使用条件请求依赖未就绪时设enabled: false跳过查询批量相关操作考虑用事务主进程侧DbService.withWriteTx处理多次更新返回函数是依赖安全的把trigger、invalidate、refetch等直接放进useCallback/useEffect依赖数组——绝不要为了规避身份抖动而把它们重新包进 ref 或从依赖中省略。延伸阅读DataApi 系统总览——渲染进程与主进程的完整架构分层、适用边界与“非数据副作用硬规则”DataApi 主进程实现——服务端 Handler → Service → SQLite 的落地模式数据分页指南——偏移 vs 游标的权威规范、线上契约与服务端 codecAPI 设计指南——RESTful 约定与查询参数线上格式API 类型系统——分页类型、守卫与Infer*辅助类型useDataApi 测试 与 DataApiService 测试——缓存 key 等价、模式断言、变更通知扇出等契约的实测验证。【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表