ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

TanStack Router 与 TanStack Query 集成实战:kitchen-sink-react-query 全功能示例深度解析

TanStack Router 与 TanStack Query 集成实战:kitchen-sink-react-query 全功能示例深度解析 TanStack Router 与 TanStack Query 集成实战kitchen-sink-react-query 全功能示例深度解析【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router本文以仓库 examples/react/kitchen-sink-react-query 示例为蓝本系统讲解如何将 TanStack Router 与 TanStack Query 组合使用构建一个集路由加载器、服务端状态缓存、乐观更新、路径参数与搜索参数校验、无路径布局、代码分割与认证守卫于一体的完整 SPA。读完本文你将掌握loader与queryOptions/ensureQueryData的协作模式、useSuspenseQuery的 suspense 数据流、URL 驱动的状态持久化以及 mutation 后缓存失效的标准写法并能在自己的项目中直接复用这套模式。示例概览一个厨房水槽式的集成样板Kitchen Sink厨房水槽在 TanStack 生态中意指把所有东西都塞进去的综合示例。这个示例的目标是在一个文件src/main.tsx约 1123 行中集中演示路由与数据层的全部主流玩法高级路由模式嵌套布局、无路径布局Pathless Layout、路径参数、搜索参数校验与持久化TanStack Query 集成通过 Routercontext注入QueryClient在loader中预取数据复杂数据获取场景列表 详情两级取数、按 URL 状态过滤/排序缓存管理queryOptions复用、mutation 成功后invalidateQueries乐观更新与提交状态useMutation的status/variables/submittedAt驱动 UI错误处理路由级defaultErrorComponent与数据层异常抛出加载状态路由级defaultPendingComponent、MatchRoute悬停预载指示器、可调的延迟仿真面板。该示例运行时的真实数据来自 src/mockTodos.ts它通过 redaxios 请求 jsonplaceholder 公共接口并用loaderDelayFn/actionDelayFn注入可配置的人工延迟方便在本地观察各种加载与提交状态。快速启动与构建示例的脚本定义在 package.json# 安装依赖 pnpm install # 启动开发服务器Vite端口 3000 pnpm dev # 生产构建vite build tsc --noEmit含类型检查 pnpm build # 预览生产构建产物 pnpm preview关键依赖tanstack/react-router约 v1.170、tanstack/react-query约 v5.102、tanstack/react-query-devtools、tanstack/react-router-devtools、react/react-domv19、zodv4、immer、redaxios样式采用 Tailwind CSS v4由 vite.config.js 中的tailwindcss/vite插件注入。TypeScript 配置见 tsconfig.jsonstrict: true、jsx: react-jsx、moduleResolution: Bundler。如果希望基于该示例新建项目原文档提供了npx gitpick脚手架方式可直接以该示例为模板生成同名项目。数据层先声用 queryOptions 统一查询定义示例把所有查询定义收敛为返回queryOptions(...)的函数这是 React Query v5 推荐的模式——查询键、查询函数与选项集中一处可在组件、loader 与 mutation 失效逻辑中安全复用。const invoicesQueryOptions () queryOptions({ queryKey: [invoices], queryFn: () fetchInvoices(), }) const invoiceQueryOptions (invoiceId: number) queryOptions({ queryKey: [invoices, invoiceId], queryFn: () fetchInvoiceById(invoiceId), }) const usersQueryOptions ({ filterBy, sortBy }) queryOptions({ queryKey: [users, { filterBy, sortBy }], queryFn: () fetchUsers({ filterBy, sortBy }), })注意查询键的层次设计列表用[invoices]详情用[invoices, invoiceId]这样invalidateQueries以[invoices]为前缀时能同时失效列表与所有详情。用户列表把筛选条件{ filterBy, sortBy }编码进查询键使排序/过滤变化天然产生新缓存项。桥接 Router 与 Querycontext 注入与 loader 预取通过 RouteContext 传递 QueryClient示例使用createRootRouteWithContext声明路由上下文类型确保所有loader与beforeLoad都能类型安全地拿到queryClientconst rootRoute createRootRouteWithContext{ auth: Auth queryClient: QueryClient }()({ component: RootComponent, }) const router createRouter({ routeTree, defaultPendingComponent: () Spinner /, defaultErrorComponent: ({ error }) ErrorComponent error{error} /, context: { auth: undefined!, queryClient }, defaultPreload: intent, defaultPreloadStaleTime: 0, scrollRestoration: true, })最终渲染时App组件用QueryClientProvider包裹RouterProvider并把运行时创建的auth注入contextsrc/main.tsx 底部QueryClientProvider client{queryClient} RouterProvider router{router} defaultPreloadintent context{{ auth }} / /QueryClientProviderloader 中预热缓存ensureQueryData路由加载器通过opts.context.queryClient.ensureQueryData(...)把查询播种进 Query 缓存。ensureQueryData的语义是若缓存中已有可用数据则直接复用否则立即发起请求——这正是路由导航期间数据就绪再渲染的关键const dashboardIndexRoute createRoute({ getParentRoute: () dashboardLayoutRoute, path: /, loader: (opts) opts.context.queryClient.ensureQueryData(invoicesQueryOptions()), component: DashboardIndexComponent, })同一查询在组件内再由useSuspenseQuery消费function DashboardIndexComponent() { const invoicesQuery useSuspenseQuery(invoicesQueryOptions()) const invoices invoicesQuery.data ... }由于 loader 已用ensureQueryData预取组件挂载时useSuspenseQuery命中缓存、不会二次发请求而ensureQueryData本身返回 Promiseloader 未完成前路由不会渲染从而实现了路由只有在数据与元素都就绪后才渲染的 suspense 式体验这正是首页文案所描述的 UX。为什么不设 loader 缓存// Since were using React Query, we dont want loader calls to ever be stale // This will ensure that the loader is always called when the route is preloaded or visited defaultPreloadStaleTime: 0,在同时使用 React Query 时路由 loader 的职责只是把查询灌入 Query 缓存真正的缓存/失效交给 Query 管理因此把defaultPreloadStaleTime设为 0让 loader 在预载或访问时始终执行避免 loader 层二次缓存造成数据不一致。搜索参数驱动的数据视图校验、持久化与 URL 状态Zod 校验搜索参数users布局路由用 zod 对象校验并解析usersView搜索参数非法值会被过滤const usersLayoutRoute createRoute({ getParentRoute: () dashboardLayoutRoute, path: users, validateSearch: z.object({ usersView: z .object({ sortBy: z.enum([name, id, email]).optional(), filterBy: z.string().optional(), }) .optional(), }).parse, ... })retainSearchParams搜索参数跨路由持久化search: { // Retain the usersView search param while navigating within or to this route (or its children!) middlewares: [retainSearchParams([usersView])], }, loaderDeps: ({ search }) ({ filterBy: search.usersView?.filterBy, sortBy: search.usersView?.sortBy, }), loader: (opts) opts.context.queryClient.ensureQueryData(usersQueryOptions(opts.deps)),retainSearchParams([usersView])是搜索参数中间件进入该路由或其子路由时usersView会自动从上一个位置保留子路由导航如进入某个用户详情不会丢失排序/过滤状态。loaderDeps则把 URL 中的排序/过滤条件声明为 loader 依赖条件变化时 loader 重新执行并触发ensureQueryData拉取新查询键的数据。URL 即状态排序与过滤的完整闭环组件内通过useNavigate把用户操作写回 URLreplace: true避免撑爆历史栈URL 又反过来驱动查询与渲染const setSortBy (sortBy: UsersSortBy) navigate({ search: (old) ({ ...old, usersView: { ...(old.usersView ?? {}), sortBy } }), replace: true, })useSuspenseQuery直接消费useLoaderDeps()的结果保证 URL 条件与数据一一对应。用户列表还演示了草稿输入 effect 同步的受控模式filterDraft本地暂存useEffect中节流式写回usersView.filterBy避免每个按键都导航。mutation 与缓存失效创建、更新与提交状态 UI创建发票成功后全量失效const useCreateInvoiceMutation () { return useMutation({ mutationKey: [invoices, create], mutationFn: postInvoice, onSuccess: () queryClient.invalidateQueries(), }) }注意onSuccess中调用的是queryClient.invalidateQueries()无参数因为它捕获的是模块顶层定义的queryClient单例。示例中 mutation 成功后直接全量失效——在演示场景简单直接但生产环境更推荐限定前缀如invalidateQueries({ queryKey: [invoices] })。表单提交后按钮根据createInvoiceMutation.status显示Creating Spinner 并禁用成功后展示绿色Created!徽标失败展示红色Failed to create.徽标src/mockTodos.ts 中标题含 error 时会抛错可用于演练失败分支const formData new FormData(event.target as HTMLFormElement) createInvoiceMutation.mutate({ title: formData.get(title) as string, body: formData.get(body) as string, })更新发票短 TTL 与乐观变量回显const useUpdateInvoiceMutation (invoiceId: number) { return useMutation({ mutationKey: [invoices, update, invoiceId], mutationFn: patchInvoice, onSuccess: () queryClient.invalidateQueries(), gcTime: 1000 * 10, // 10 秒后回收该 mutation 缓存 }) }gcTime: 1000 * 10控制 mutation 状态保留时长演示 10 秒后回收。提交后 UI 通过updateInvoiceMutation.variables?.id invoice.id判断当前行并用submittedAt作为 key 触发Saved!/Failed to save.徽标。表单字段在status pending时禁用避免提交期间二次编辑。服务端的发票补丁操作由 src/mockTodos.ts 的patchInvoice使用 immer 的produce完成不可变更新标题含 error 同样会抛错以便演练失败态。路径参数与 URL 即备忘录Invoice 详情路由路径参数解析与搜索参数$invoiceId路由展示路径参数 搜索参数双校验const invoiceRoute createRoute({ getParentRoute: () invoicesLayoutRoute, path: $invoiceId, params: { parse: (params) ({ invoiceId: z.number().int().parse(Number(params.invoiceId)), }), stringify: ({ invoiceId }) ({ invoiceId: ${invoiceId} }), }, validateSearch: (search) z.object({ showNotes: z.boolean().optional(), notes: z.string().optional(), }).parse(search), loader: (opts) opts.context.queryClient.ensureQueryData( invoiceQueryOptions(opts.params.invoiceId), ), component: InvoiceComponent, })params.parse/stringify把字符串路径参数与强类型对象互转这里把invoiceId规范化为数字validateSearch声明可选的showNotes折叠面板与notes便签文本。便签写入 URL可分享的状态组件内便签输入通过useEffect同步到 URL 搜索参数实现了便签存在 URL 里——复制 URL 到新标签页即可恢复便签内容const [notes, setNotes] React.useState(search.notes ?? ) React.useEffect(() { navigate({ search: (old) ({ ...old, notes: notes ? notes : undefined }), replace: true, params: true, }) }, [notes])详情页还演示了从列表页直达首页的 1 New Invoice 按钮用Link to{invoiceRoute.to} params{{ invoiceId: 3 }}带参跳转列表项则用MatchRoute在目标路由 pending 时渲染 Spinner实现行内加载指示MatchRoute to{invoiceRoute.to} params{{ invoiceId: invoice.id }} pending {(match) Spinner show{!!match} waitdelay-50 /} /MatchRoute全局导航体验Spinner、预载与滚动恢复路由过渡指示器RouterSpinner通过useRouterState订阅路由状态status pending时显示全局 Spinnerfunction RouterSpinner() { const isLoading useRouterState({ select: (s) s.status pending }) return Spinner show{isLoading} / }悬停预载导航链接统一开启preloadintent鼠标悬停/触摸聚焦即预载与defaultPreload: intent、defaultPreloadStaleTime: 0配合悬停时触发 loader →ensureQueryData预热 Query 缓存真正点击导航时数据往往已就绪。路由加载期间的兜底 UI 由defaultPendingComponent提供。滚动恢复createRouter({ ..., scrollRestoration: true })开启滚动位置恢复导航返回时还原滚动位置示例根组件 src/main.tsx 中配置。无路径布局、代码分割与认证守卫无路径布局Pathless Layoutauth与pathlessLayout两个无路径布局演示用 id 而非路径组织布局auth布局在beforeLoad中做认证守卫其子路由profilepathlessLayout布局下挂route-a/route-b两个子页面共享同一套外层 UIconst authPathlessLayoutRoute createRoute({ getParentRoute: () rootRoute, id: auth, beforeLoad: ({ context, location }) { if (context.auth.status loggedOut) { throw redirect({ to: loginRoute.to, search: { redirect: location.href }, }) } return { username: auth.username } }, })要点未登录访问/profile时抛出redirect到登录页并把当前location.href放进redirect搜索参数登录成功后router.invalidate()重新校验并回跳。注释特别提醒应使用location.href而非router.state.resolvedLocation后者可能滞后于实际位置。异步组件与代码分割expensive路由演示按需加载组件通过lazyRouteComponent(() import(./Expensive))异步导入实现见 src/Expensive.tsx构建时被拆分为独立 chunk首次进入才下载执行。登录态与上下文注入auth是一个模块级的可变单例对象src/main.tsx 中的login/logout/status/username经RouterProvider context注入LoginComponent通过loginRoute.useRouteContext({ select: ... })读取登录成功后在useLayoutEffect中router.history.push(search.redirect)完成回跳。延迟仿真面板可调的加载体验实验室App组件左下角内置了一个用sessionStorage持久化的仿真控制台src/main.tsxloaderDelay数据请求延迟Fast150ms / Fast 3G500ms / Slow 3G2000ms可滑动 0–5000ms由 src/utils.tsx 的loaderDelayFn读取defaultPendingMs/defaultPendingMinMs传给RouterProvider控制路由 pending 组件的延迟出现与最短展示时长模拟慢速下先显示骨架、快速下不闪烁的体验actionDelay由actionDelayFn读取模拟提交请求耗时。配合useSessionStorage辅助 hook写入即 JSON 序列化所有调节都会刷新后保留方便对比不同网络环境下的数据流与 UI 表现。路由树一览与延伸阅读完整路由树在 src/main.tsx 底部组装可作为路由布局层级的对照索引const routeTree rootRoute.addChildren([ indexRoute, dashboardLayoutRoute.addChildren([ dashboardIndexRoute, invoicesLayoutRoute.addChildren([invoicesIndexRoute, invoiceRoute]), usersLayoutRoute.addChildren([usersIndexRoute, userRoute]), ]), expensiveRoute, authPathlessLayoutRoute.addChildren([profileRoute]), loginRoute, pathlessLayoutRoute.addChildren([pathlessLayoutARoute, pathlessLayoutBRoute]), ])本文所有代码均出自 examples/react/kitchen-sink-react-query核心实现集中于 src/main.tsx路由定义、组件、devtools 装配、src/mockTodos.ts数据层与延迟仿真、src/utils.tsx延迟工具。仓库中另有 examples/react/kitchen-sink-react-query 的同源变体kitchen-sink、kitchen-sink-file-based、kitchen-sink-react-query-file-based等分别对应手写路由树、文件式路由与 React Query 的不同组合可对照阅读。【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表