资讯动态

Claude Code Infrastructure Showcase 前端数据获取实战:TanStack Query 的 Suspense 模式与缓存优先策略完整指南

发布时间:2026/10/9 1:17:02 来源:尧图企业网站定制
AI 技能AI 插件人工智能开发工具【免费下载链接】claude-code-infrastructure-showcaseExamples of my Claude Code infrastructure with skill auto-activation, hooks, and agents项目地址https://gitcode.com/gh_mirrors/cl/claude-code-infrastructure-showcase点击查看免费下载本文以claude-code-infrastructure-showcase仓库中随frontend-dev-guidelines技能一起交付的数据获取指南>import { useSuspenseQuery } from tanstack/react-query; import { myFeatureApi } from ../api/myFeatureApi; export const MyComponent: React.FCProps ({ id }) { // 无需 isLoading —— Suspense 已经替你处理 const { data } useSuspenseQuery({ queryKey: [myEntity, id], queryFn: () myFeatureApi.getEntity(id), }); // 这里的 data 永远是已定义的值类型是 Data而不是 Data | undefined return div{data.name}/div; }; // 外面包上 Suspense 边界 SuspenseLoader MyComponent id{123} / /SuspenseLoader关键点因为useSuspenseQuery在数据就绪前会让组件“挂起suspend”所以渲染到 DOM 时data必定有值无需判空、无需isLoading分支——这是它相比传统useQuery在代码整洁度上的决定性优势。useSuspenseQuery vs useQueryFeatureuseSuspenseQueryuseQueryLoading stateHandled by SuspenseManualisLoadingcheckData typeAlways definedData \| undefinedUse withSuspense boundariesTraditional componentsRecommended forNEW componentsLegacy code onlyError handlingError boundariesManual error state什么时候仍使用普通 useQuery维护遗留代码非常简单的、不需要 Suspense 的场景需要后台轮询polling更新的场景对于新组件永远优先 useSuspenseQuery。佐证仓库加载状态指南 loading-and-error-states.md 中明确将“早期返回isLoading分支”列为反模式会导致布局偏移、CLS 指标恶化、滚动位置丢失并把SuspenseLoaderuseSuspenseQuery列为唯一推荐方案同时提供多 Suspense 边界各区块独立加载与嵌套 Suspense 的用法。Cache-First 策略先查缓存再打 API“智能缓存”通过先检查 React Query 缓存来减少 API 调用次数。典型场景用户刚从列表页进入详情页详情数据其实已经在列表缓存里了不必再发一次请求。import { useSuspenseQuery, useQueryClient } from tanstack/react-query; import { postApi } from ../api/postApi; export function useSuspensePost(postId: number) { const queryClient useQueryClient(); return useSuspenseQuery({ queryKey: [post, postId], queryFn: async () { // 策略 1先从列表缓存中找 const cachedListData queryClient.getQueryData{ posts: Post[] }([ posts, list ]); if (cachedListData?.posts) { const cachedPost cachedListData.posts.find( (post) post.id postId ); if (cachedPost) { return cachedPost; // 命中缓存直接返回 } } // 策略 2缓存未命中回源 API return postApi.getPost(postId); }, staleTime: 5 * 60 * 1000, // 5 分钟内视为新鲜数据 gcTime: 10 * 60 * 1000, // 未使用数据在缓存中保留 10 分钟 refetchOnWindowFocus: false, // 窗口聚焦时不重新请求 }); }关键参数语义在调用 API之前先检查 grid/list 缓存避免冗余请求staleTime数据被视为“新鲜”的时长新鲜期内重复挂载不会重新请求gcTime未被使用无观察者的数据在缓存中保留的时长注意旧版本 TanStack Query 中该参数名为cacheTimerefetchOnWindowFocus: false按团队偏好切窗口回来不强制刷新纵深背景README 中描述该模式来源于带“复杂数据表格”的生产前端。文档后续的完整示例useSuspensePost也演示了先查[posts-v2, blogId, summary]/[posts-v2, blogId, flat]两种视图的 grid 缓存按S_ID匹配行命中即复用未命中才回源postApi.getPost(blogId, postId)。从源码结构看这套“列表缓存反哺详情页”的设计是文档中 Query Key 组织规范见下文的直接应用。并行数据获取useSuspenseQueries当需要同时获取多个相互独立的数据资源时使用useSuspenseQueriesimport { useSuspenseQueries } from tanstack/react-query; export const MyComponent: React.FC () { const [userQuery, settingsQuery, preferencesQuery] useSuspenseQueries({ queries: [ { queryKey: [user], queryFn: () userApi.getCurrentUser(), }, { queryKey: [settings], queryFn: () settingsApi.getSettings(), }, { queryKey: [preferences], queryFn: () preferencesApi.getPreferences(), }, ], }); // 所有数据均已就绪Suspense 处理加载过程 const user userQuery.data; const settings settingsQuery.data; const preferences preferencesQuery.data; return Display user{user} settings{settings} prefs{preferences} /; };收益所有查询并行发起只需一个 Suspense 边界结果类型安全每个 query 返回独立的、类型明确的 data完整示例可参考 complete-examples.md 中的 Dashboard 示例stats、users/active、activity/recent三个查询并行加载后分别渲染到三个Grid卡片。Query Keys 组织规范一致性是第一原则Query Key 是 React Query 缓存的核心标识命名是否一致直接决定缓存复用与失效invalidation是否有效。命名约定// 实体列表 [entities, blogId] [entities, blogId, summary] // 带视图模式 [entities, blogId, flat] // 单个实体 [entity, blogId, entityId] // 关联数据 [entity, entityId, history] [entity, entityId, comments] // 用户相关 [user, userId, profile] [user, userId, permissions]规则以实体名开头列表用复数单个用单数用 ID 增加精确性视图模式 / 关联关系放在末尾全应用保持一致的约定Query Key 使用示例// 来自 useSuspensePost.ts queryKey: [post, blogId, postId] queryKey: [posts-v2, blogId, summary] // 失效模式invalidation patterns queryClient.invalidateQueries({ queryKey: [post, blogId] }); // 使该博客下所有帖子失效如表单页 queryClient.invalidateQueries({ queryKey: [post] }); // 使所有帖子失效原理invalidateQueries采用“前缀匹配”语义——传入[post, blogId]会命中所有以该前缀开头的 key包括[post, blogId, postId]这是“改一个、刷一批”的关键机制。因为[post, blogId, postId]与[post, blogId]共享前缀详情页、列表页、表单页才能通过一次失效同步刷新。API Service Layer 模式每个功能一个集中式服务层所有 API 调用不应散落在组件里而应集中到按 feature 组织的服务层文件。文件结构features/ my-feature/ api/ myFeatureApi.ts # Service layer服务模式源自 postApi.ts/** * Centralized API service for my-feature operations * Uses apiClient for consistent error handling */ import apiClient from /lib/apiClient; import type { MyEntity, UpdatePayload } from ../types; export const myFeatureApi { /** * Fetch a single entity */ getEntity: async (blogId: number, entityId: number): PromiseMyEntity { const { data } await apiClient.get( /blog/entities/${blogId}/${entityId} ); return data; }, /** * Fetch all entities for a form */ getEntities: async (blogId: number, view: summary | flat): PromiseMyEntity[] { const { data } await apiClient.get( /blog/entities/${blogId}, { params: { view } } ); return data.rows; }, /** * Update entity */ updateEntity: async ( blogId: number, entityId: number, payload: UpdatePayload ): PromiseMyEntity { const { data } await apiClient.put( /blog/entities/${blogId}/${entityId}, payload ); return data; }, /** * Delete entity */ deleteEntity: async (blogId: number, entityId: number): Promisevoid { await apiClient.delete(/blog/entities/${blogId}/${entityId}); }, };关键要点导出单个包含方法的对象统一使用apiClient来自/lib/apiClient的 axios 实例参数与返回值全部类型安全每个方法带 JSDoc 注释错误处理集中由 apiClient 统一处理关联规范feature 目录结构api/、components/、hooks/、helpers/、types/在 file-organization.md 中有完整展开导入别名/对应src/、~types、~components、~features的约定在 SKILL.md 的 Import Aliases Quick Reference 表中登记该表来自原生产项目的 Vite 配置。路由格式规则IMPORTANT下面的路径示例来自原生产项目基于前缀的微服务路由架构代理proxy把每个服务映射到各自的路径前缀。请替换为你自己的 API 路径——如果后端路由挂在/api/下就用/api/前缀。正确格式// ✅ 正确 —— 路径与代理配置匹配此处为直接服务前缀 await apiClient.get(/blog/posts/123); await apiClient.post(/projects/create, data); await apiClient.put(/users/update/456, updates); await apiClient.get(/email/templates); // ❌ 错误 —— 在该代理配置下不会追加 /api/ 前缀 await apiClient.get(/api/blog/posts/123); // 在此项目中错误若你的后端用 /api/ 则没问题 await apiClient.post(/api/projects/create, data); // 在此项目中错误原项目的微服务路由示例表单服务/blog/*项目服务/projects/*邮件服务/email/*用户服务/users/*为什么在该架构中API 路由由代理配置处理因此无需/api/前缀。真正的规则是让apiClient的路径匹配你自己的路由设置。注意区分文档中“正确/错误”是针对原项目代理配置而言的并非普适规则。文章示例代码中 API 服务层使用的/blog/entities/...等路径均与该前缀式微服务路由保持一致——你在落地时必须对齐自己的后端网关配置这是该章节强调的核心。Mutations写操作的统一范式基本 Mutation 模式import { useMutation, useQueryClient } from tanstack/react-query; import { myFeatureApi } from ../api/myFeatureApi; import { useMuiSnackbar } from /hooks/useMuiSnackbar; export const MyComponent: React.FC () { const queryClient useQueryClient(); const { showSuccess, showError } useMuiSnackbar(); const updateMutation useMutation({ mutationFn: (payload: UpdatePayload) myFeatureApi.updateEntity(blogId, entityId, payload), onSuccess: () { // 失效并重新获取 queryClient.invalidateQueries({ queryKey: [entity, blogId, entityId] }); showSuccess(Entity updated successfully); }, onError: (error) { showError(Failed to update entity); console.error(Update error:, error); }, }); const handleUpdate () { updateMutation.mutate({ name: New Name }); }; return ( Button onClick{handleUpdate} disabled{updateMutation.isPending} {updateMutation.isPending ? Updating... : Update} /Button ); };模式要点mutationFn调用服务层onSuccess里invalidateQueries 使相关查询失效并用useMuiSnackbar提示成功onError提示失败并打日志按钮用isPending控制禁用态与文案。乐观更新Optimistic Updatesconst updateMutation useMutation({ mutationFn: (payload) myFeatureApi.update(id, payload), // 乐观更新 onMutate: async (newData) { // 取消进行中的重新获取 await queryClient.cancelQueries({ queryKey: [entity, id] }); // 快照当前值 const previousData queryClient.getQueryData([entity, id]); // 乐观写入缓存 queryClient.setQueryData([entity, id], (old) ({ ...old, ...newData, })); // 返回回滚函数所需上下文 return { previousData }; }, // 出错时回滚 onError: (err, newData, context) { queryClient.setQueryData([entity, id], context.previousData); showError(Update failed); }, // 成功或失败后都重新获取 onSettled: () { queryClient.invalidateQueries({ queryKey: [entity, id] }); }, });乐观更新三步曲onMutate先取消在途请求 → 快照旧值 →setQueryData立即写入新值并返回上下文onError用上下文中的快照回滚onSettled无论成败都失效重取确保最终与服务端一致。高级查询模式预取Prefetchingexport function usePrefetchEntity() { const queryClient useQueryClient(); return (blogId: number, entityId: number) { return queryClient.prefetchQuery({ queryKey: [entity, blogId, entityId], queryFn: () myFeatureApi.getEntity(blogId, entityId), staleTime: 5 * 60 * 1000, }); }; } // 用法鼠标悬停时预取 div onMouseEnter{() prefetch(blogId, id)} Link to{/entity/${id}}View/Link /div悬停预取让用户点击进入详情页时数据已就绪页面几乎秒开。不发起请求的纯缓存读取export function useEntityFromCache(blogId: number, entityId: number) { const queryClient useQueryClient(); // 从缓存取未命中也不发请求 const directCache queryClient.getQueryDataMyEntity([entity, blogId, entityId]); if (directCache) return directCache; // 再试 grid 缓存 const gridCache queryClient.getQueryData{ rows: MyEntity[] }([entities-v2, blogId]); return gridCache?.rows.find(row row.id entityId); }适用于“有缓存就用、没有也别请求”的场景如渲染部分占位内容。依赖查询Dependent Queries// 先获取 user再获取该用户的设置 const { data: user } useSuspenseQuery({ queryKey: [user, userId], queryFn: () userApi.getUser(userId), }); const { data: settings } useSuspenseQuery({ queryKey: [user, userId, settings], queryFn: () settingsApi.getUserSettings(user.id), // Suspense 机制自动等待 user 加载完成 });由于useSuspenseQuery会挂起组件直到数据就绪第二个查询天然“等待”第一个查询完成——无需手写enabled标志或 Promise 链。注意此时user.id在类型上也是安全的已定义。API Client 配置只用 apiClientimport apiClient from /lib/apiClient; // apiClient 是配置好的 axios 实例 // 自动包含 // - Base URL 配置 // - 基于 Cookie 的认证 // - 错误拦截器 // - 响应转换器不要新建 axios 实例——为了行为一致一律复用apiClient。佐证SKILL.md 中导入别名表将/映射到src/即import { apiClient } from /lib/apiClientAPI 服务层myFeatureApi也统一从该实例发出请求保证错误拦截与认证逻辑只存在一处。查询中的错误处理onError 回调import { useMuiSnackbar } from /hooks/useMuiSnackbar; const { showError } useMuiSnackbar(); const { data } useSuspenseQuery({ queryKey: [entity, id], queryFn: () myFeatureApi.getEntity(id), // 处理错误 onError: (error) { showError(Failed to load entity); console.error(Load error:, error); }, });Error Boundaries与 Error Boundary 组合形成完整的错误处理链import { ErrorBoundary } from react-error-boundary; ErrorBoundary fallback{ErrorDisplay /} onError{(error) console.error(error)} SuspenseLoader ComponentWithSuspenseQuery / /SuspenseLoader /ErrorBoundary关联规范加载与错误状态专题 loading-and-error-states.md 强调“用户反馈一律使用useMuiSnackbar严禁使用 react-toastify”并提供ErrorBoundary的完整ErrorFallback组件模板含错误消息展示与 “Try Again” 重置按钮useMuiSnackbar支持showSuccess/showError/showWarning/showInfo四种提示。常见模式表单、对话框、useAuth见 common-patterns.md。完整示例示例 1简单实体获取import React from react; import { useSuspenseQuery } from tanstack/react-query; import { Box, Typography } from mui/material; import { userApi } from ../api/userApi; interface UserProfileProps { userId: string; } export const UserProfile: React.FCUserProfileProps ({ userId }) { const { data: user } useSuspenseQuery({ queryKey: [user, userId], queryFn: () userApi.getUser(userId), staleTime: 5 * 60 * 1000, }); return ( Box Typography varianth5{user.name}/Typography Typography{user.email}/Typography /Box ); }; // 与 Suspense 搭配使用 SuspenseLoader UserProfile userId123 / /SuspenseLoader示例 2缓存优先策略import { useSuspenseQuery, useQueryClient } from tanstack/react-query; import { postApi } from ../api/postApi; import type { Post } from ../types; /** * Hook with cache-first strategy * Checks grid cache before API call */ export function useSuspensePost(blogId: number, postId: number) { const queryClient useQueryClient(); return useSuspenseQueryPost, Error({ queryKey: [post, blogId, postId], queryFn: async () { // 1. 先检查 grid 缓存 const gridCache queryClient.getQueryData{ rows: Post[] }([ posts-v2, blogId, summary ]) || queryClient.getQueryData{ rows: Post[] }([ posts-v2, blogId, flat ]); if (gridCache?.rows) { const cached gridCache.rows.find(row row.S_ID postId); if (cached) { return cached; // 复用 grid 数据 } } // 2. 缓存未命中直接请求 return postApi.getPost(blogId, postId); }, staleTime: 5 * 60 * 1000, gcTime: 10 * 60 * 1000, refetchOnWindowFocus: false, }); }收益避免重复 API 调用数据已加载过时即刻可用缓存未命中时自动回源 API示例 3并行获取import { useSuspenseQueries } from tanstack/react-query; export const Dashboard: React.FC () { const [statsQuery, projectsQuery, notificationsQuery] useSuspenseQueries({ queries: [ { queryKey: [stats], queryFn: () statsApi.getStats(), }, { queryKey: [projects, active], queryFn: () projectsApi.getActiveProjects(), }, { queryKey: [notifications, unread], queryFn: () notificationsApi.getUnread(), }, ], }); return ( Box StatsCard data{statsQuery.data} / ProjectsList projects{projectsQuery.data} / Notifications items{notificationsQuery.data} / /Box ); };Mutations with Cache Invalidation更新与删除的标准做法更新 Mutationimport { useMutation, useQueryClient } from tanstack/react-query; import { postApi } from ../api/postApi; import { useMuiSnackbar } from /hooks/useMuiSnackbar; export const useUpdatePost () { const queryClient useQueryClient(); const { showSuccess, showError } useMuiSnackbar(); return useMutation({ mutationFn: ({ blogId, postId, data }: UpdateParams) postApi.updatePost(blogId, postId, data), onSuccess: (data, variables) { // 使具体帖子失效 queryClient.invalidateQueries({ queryKey: [post, variables.blogId, variables.postId] }); // 使列表失效以刷新 grid queryClient.invalidateQueries({ queryKey: [posts-v2, variables.blogId] }); showSuccess(Post updated); }, onError: (error) { showError(Failed to update post); console.error(Update error:, error); }, }); }; // 用法 const updatePost useUpdatePost(); const handleSave () { updatePost.mutate({ blogId: 123, postId: 456, data: { responses: { 101: value } } }); };更新成功后双失效既让单条详情失效也让列表 grid 失效——保证详情页与列表页同时拿到最新数据。删除 Mutationexport const useDeletePost () { const queryClient useQueryClient(); const { showSuccess, showError } useMuiSnackbar(); return useMutation({ mutationFn: ({ blogId, postId }: DeleteParams) postApi.deletePost(blogId, postId), onSuccess: (data, variables) { // 手动从缓存移除乐观做法 queryClient.setQueryData{ rows: Post[] }( [posts-v2, variables.blogId], (old) ({ ...old, rows: old?.rows.filter(row row.S_ID ! variables.postId) || [] }) ); showSuccess(Post deleted); }, onError: (error, variables) { // 回滚 —— 重新获取以获得准确状态 queryClient.invalidateQueries({ queryKey: [posts-v2, variables.blogId] }); showError(Failed to delete post); }, }); };删除用setQueryData直接从缓存过滤掉该行UI 立即反映出错时再invalidateQueries回滚到服务端真实状态。Query 配置最佳实践默认配置QueryClientProvider 处// In QueryClientProvider setup const queryClient new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5, // 5 分钟 gcTime: 1000 * 60 * 10, // 10 分钟旧版本名为 cacheTime refetchOnWindowFocus: false, // 窗口聚焦不重新请求 refetchOnMount: false, // 挂载时若数据仍新鲜则不重新请求 retry: 1, // 失败的查询重试一次 }, }, });按查询覆盖默认值// 频繁变化的数据 —— 缩短 staleTime useSuspenseQuery({ queryKey: [notifications, unread], queryFn: () notificationApi.getUnread(), staleTime: 30 * 1000, // 30 秒 }); // 极少变化的数据 —— 加长 staleTime useSuspenseQuery({ queryKey: [form, blogId, structure], queryFn: () formApi.getStructure(blogId), staleTime: 30 * 60 * 1000, // 30 分钟 });取舍原则数据变化越频繁staleTime越短数据越稳定如表单结构、配置类数据staleTime越长。全局默认与局部覆盖配合避免“一刀切”。在 Claude Code 工程中落地让 Agent 自动遵守这份指南这份指南的真正价值在于它不只是给人读的规范而是随技能自动激活、约束 Agent 写码行为的规则。在本仓库中技能自动激活链路UserPromptSubmit钩子 skill-activation-prompt.ts 分析每次用户提示对照 skill-rules.json 中的触发规则命中“创建 React 组件 / 获取数据”等意图时强制建议加载frontend-dev-guidelinesPreToolUse钩子 skill-verification-guard.ts 在写文件前校验强制技能是否已激活两试阻断模型。当你输入“create a new React component”这类提示词时Agent 会先加载技能再动手。技术栈探测安装向导 setup.ts 的detectTechStack会扫描目标项目package.json中的依赖其中tanstack/react-query或tanstack/react-router命中即标记 TanStack 栈setup.ts并在安装完成后提示用 “create a new React component” 验证技能自动激活——此时加载的正是这份data-fetching.md所属的技能。镜像同步.claude/skills/与.agents/skills/两份内容由 sync-agent-skills.sh 保持同步verify-setup会警告漂移详见 README.md。升级与校验重跑npx tsx setup.ts ~/my-project --yes只更新skill-rules.json的设置与依赖不会覆盖你自定义的技能文件setup.ts任何时候可运行bash .claude/scripts/verify-setup.sh做 8 项健康检查。关联技能前端服务端侧的 API 设计路由、控制器、校验遵循 backend-dev-guidelines前端错误上报集成 Sentry 遵循 error-tracking。总结现代数据获取配方Modern Data Fetching Recipe创建 API Servicefeatures/X/api/XApi.ts基于 apiClient使用 useSuspenseQuery在由SuspenseLoader包裹的组件中使用Cache-First调用 API 前先检查 grid 缓存Query Keys统一命名[entity, id]路由格式/blog/route而非/api/blog/route以你的代理/网关配置为准Mutations成功后在onSuccess中invalidateQueries错误处理onErroruseMuiSnackbar类型安全参数与返回值全部标注类型延伸阅读同技能资源文件component-patterns.md — Suspense 集成与组件模式loading-and-error-states.md — SuspenseLoader 与加载/错误状态规范complete-examples.md — 完整可运行示例file-organization.md — feature 目录组织routing-guide.md — TanStack Router 文件夹式路由common-patterns.md — 表单 / 对话框 / 认证等常见模式赞分享AI 技能AI 插件人工智能开发工具【免费下载链接】claude-code-infrastructure-showcaseExamples of my Claude Code infrastructure with skill auto-activation, hooks, and agents项目地址https://gitcode.com/gh_mirrors/cl/claude-code-infrastructure-showcase点击查看免费下载相关推荐gogcli gog sheets raw 命令详解无损导出 Google Sheets 原始 API JSONgogcli gog sheets raw 命令详解无损导出 Google Sheets 原始 API JSON 导读 gog sheets raw 是 goAI 技能AI 插件人工智能开发工具mlx-audio 中的 MiniMax Music 3从命令行到 Python 的多语言歌曲生成完整指南mlx audio 中的 MiniMax Music 3从命令行到 Python 的多语言歌曲生成完整指南 导读 MiniMax Music 3 是一个多语言AI 技能AI 插件人工智能开发工具PowerShell 跨平台安装教程Windows / Linux / macOS 三步装好 pwsh 并验证PowerShell 跨平台安装教程Windows / Linux / macOS 三步装好 pwsh 并验证 PowerShell 是微软开源的跨平台命令行AI 技能AI 插件人工智能开发工具上一篇5个强大技巧用Spicetify-CLI彻底改造你的Spotify体验下一篇nand2tetris CPU架构设计终极指南从零构建现代计算机核心创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑