React Query与React Router页面导航缓存异常问题求助
问题描述
运营小型博客时使用React Query缓存YAML内容的Fetch接口响应,出现以下异常:从文章页返回主页时缓存正常,但在文章页刷新后导航回主页,会抛出错误:
A component suspended while responding to synchronous input. This will cause the UI to be replaced with a loading indicator. To fix, updates that suspend should be wrapped with startTransition.
当前环境为Vite + React 18.2.0,已尝试startTransition包裹操作、开发者工具排查、添加<Suspense>组件,均未解决问题,寻求有效方案。
代码与栈追踪信息
入口文件代码
// eslint-disable-next-line no-unused-vars import React from "react" import Index from "./pages/Index.jsx" import { createRoot } from "react-dom/client" import { createBrowserRouter, RouterProvider } from "react-router-dom" import Error from "./pages/Error.jsx" import Post from "./pages/Post.jsx" import Minesweeper from "./pages/Minesweeper.jsx" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" const queryClient = new QueryClient() const router = createBrowserRouter([ { path: "/", element: <Index />, errorElement: <Error /> }, { path: "/posts/:postid", element: <Post />, errorElement: <Error /> }, { path: "/minesweeper", element: <Minesweeper />, } ]) const root = createRoot(document.getElementById("root")) root.render( <QueryClientProvider client={queryClient}> <RouterProvider router={router} /> </QueryClientProvider> )
主页缓存组件代码
import React, { startTransition } from "react" import matter from "gray-matter" import { Link } from "react-router-dom" import { useQuery } from "@tanstack/react-query" const sort = (a, b) => { if (a.data.date < b.data.date) { return 1 } return -1 } async function getPosts() { try { let masterlist = await fetch("/posts/masterlist.json") const masterlistJSON = await masterlist.json() const posts = masterlistJSON.map((post) => `/posts/${post}`) const headers = new Headers() headers.set("Accept", "text/markdown") const postURLArrayMap = await Promise.all( posts.map((post) => ( fetch(post, {headers}) .then((res) => res.text()) )) ) const parsedPosts = postURLArrayMap.map((post) => matter(post)) parsedPosts.sort(sort) return parsedPosts } catch (err) { console.error(err) return err } } export default function IndexPosts() { const { isPending, isError, data, error } = useQuery({ queryKey: ["posts"], queryFn: async () => { return await getPosts() }, }) if (isPending) { return <></> } if (isError) { return <h4>{error.message}</h4> } return ( <React.Fragment> <h2 className="highlighted">articles</h2> <div id="articles"> { data.map((post, index) => ( <article key={index}> <div className="card"> <img src="/graphics/anon.png" alt="tumblr anonymous grey face with sunglasses" height={80} width={80} /> <Link to={`/posts/${post.data.name}`}>{post.data.title}</Link> <p>{new Date(post.data.date).toLocaleDateString()}</p> </div> </article> )) } </div> </React.Fragment> ) }
完整栈追踪信息
React Router caught the following error during render Error: A component suspended while responding to synchronous input. This will cause the UI to be replaced with a loading indicator. To fix, updates that suspend should be wrapped with startTransition. throwException chunk-RZQHDLBT.js:21657 handleError chunk-RZQHDLBT.js:26470 renderRootSync chunk-RZQHDLBT.js:26555 recoverFromConcurrentError chunk-RZQHDLBT.js:26172 performSyncWorkOnRoot chunk-RZQHDLBT.js:26315 flushSyncCallbacks chunk-RZQHDLBT.js:16575 ensureRootIsScheduled chunk-RZQHDLBT.js:26063 ensureRootIsScheduled chunk-RZQHDLBT.js:26061 scheduleUpdateOnFiber chunk-RZQHDLBT.js:25998 dispatchSetState chunk-RZQHDLBT.js:20466 setState react-router-dom.js:4529 optInStartTransition react-router-dom.js:4509 setState react-router-dom.js:4529 updateState react-router-dom.js:1276 updateState react-router-dom.js:1276 completeNavigation react-router-dom.js:1351 handleLoaders react-router-dom.js:1616 startNavigation react-router-dom.js:1524 navigate react-router-dom.js:1427 navigate react-router-dom.js:3850 useLinkClickHandler react-router-dom.js:5013 handleClick react-router-dom.js:4829 callCallback2 chunk-RZQHDLBT.js:11114 invokeGuardedCallbackDev chunk-RZQHDLBT.js:11139 invokeGuardedCallback chunk-RZQHDLBT.js:11173 invokeGuardedCallbackAndCatchFirstError chunk-RZQHDLBT.js:11176 executeDispatch chunk-RZQHDLBT.js:14456 processDispatchQueueItemsInOrder chunk-RZQHDLBT.js:14476 processDispatchQueue chunk-RZQHDLBT.js:14485 dispatchEventsForPlugins chunk-RZQHDLBT.js:14493 dispatchEventForPluginEventSystem chunk-RZQHDLBT.js:14617 batchedUpdates$1 chunk-RZQHDLBT.js:26349 batchedUpdates chunk-RZQHDLBT.js:11019 dispatchEventForPluginEventSystem chunk-RZQHDLBT.js:14616 dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay chunk-RZQHDLBT.js:12918 dispatchEvent chunk-RZQHDLBT.js:12912 dispatchDiscreteEvent chunk-RZQHDLBT.js:12889 Object { componentStack: "\nLazy\ndiv\nfooter\ndiv\nmain\nIndex\nRenderedRoute@http://localhost:3000/node_modules/.vite/deps/react-router-dom.js?v=d9949c6e:3560:7\nRenderErrorBoundary@http://localhost:3000/node_modules/.vite/deps/react-router-dom.js?v=d9949c6e:3517:5\nDataRoutes@http://localhost:3000/node_modules/.vite/deps/react-router-dom.js?v=d9949c6e:4663:7\nRouter@http://localhost:3000/node_modules/.vite/deps/react-router-dom.js?v=d9949c6e:3942:7\nRouterProvider@http://localhost:3000/node_modules/.vite/deps/react-router-dom.js?v=d9949c6e:4480:7\nQueryClientProvider@http://localhost:3000/node_modules/.vite/deps/@tanstack_react-query.js?v=d9949c6e:2621:27" } react-router-dom.js:3554:12 componentDidCatch react-router-dom.js:3554 callback chunk-RZQHDLBT.js:21528 callCallback chunk-RZQHDLBT.js:17951 commitUpdateQueue chunk-RZQHDLBT.js:17968 commitLayoutEffectOnFiber chunk-RZQHDLBT.js:24505 commitLayoutMountEffects_complete chunk-RZQHDLBT.js:25416 commitLayoutEffects_begin chunk-RZQHDLBT.js:25405 commitLayoutEffects chunk-RZQHDLBT.js:25356 commitRootImpl chunk-RZQHDLBT.js:26789 commitRoot chunk-RZQHDLBT.js:26713 performSyncWorkOnRoot chunk-RZQHDLBT.js:26331 flushSyncCallbacks chunk-RZQHDLBT.js:16575 ensureRootIsScheduled chunk-RZQHDLBT.js:26063 (Async: VoidFunction) ensureRootIsScheduled chunk-RZQHDLBT.js:26061 scheduleUpdateOnFiber chunk-RZQHDLBT.js:25998 dispatchSetState chunk-RZQHDLBT.js:20466 setState react-router-dom.js:4529 optInStartTransition react-router-dom.js:4509 setState react-router-dom.js:4529 updateState react-router-dom.js:1276 updateState react-router-dom.js:1276 completeNavigation react-router-dom.js:1351 handleLoaders react-router-dom.js:1616 startNavigation react-router-dom.js:1524 navigate react-router-dom.js:1427 navigate react-router-dom.js:3850 useLinkClickHandler react-router-dom.js:5013 handleClick react-router-dom.js:4829 callCallback2 chunk-RZQHDLBT.js:11114 invokeGuardedCallbackDev chunk-RZQHDLBT.js:11139 invokeGuardedCallback chunk-RZQHDLBT.js:11173 invokeGuardedCallbackAndCatchFirstError chunk-RZQHDLBT.js:11176 executeDispatch chunk-RZQHDLBT.js:14456 processDispatchQueueItemsInOrder chunk-RZQHDLBT.js:14476 processDispatchQueue chunk-RZQHDLBT.js:14485 dispatchEventsForPlugins chunk-RZQHDLBT.js:14493 dispatchEventForPluginEventSystem chunk-RZQHDLBT.js:14617 batchedUpdates$1 chunk-RZQHDLBT.js:26349 batchedUpdates chunk-RZQHDLBT.js:11019 dispatchEventForPluginEventSystem chunk-RZQHDLBT.js:14616 dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay chunk-RZQHDLBT.js:12918 dispatchEvent chunk-RZQHDLBT.js:12912 dispatchDiscreteEvent chunk-RZQHDLBT.js:12889
解决方案
1. 启用React Query Suspense模式并添加边界
React Query默认不开启Suspense,需显式配置并配合
// 修改IndexPosts组件的useQuery配置 export default function IndexPosts() { const { isError, data, error } = useQuery({ queryKey: ["posts"], queryFn: getPosts, suspense: true, // 启用Suspense模式 }) if (isError) { return <h4>{error.message}</h4> } return ( <React.Fragment> {/* 原组件内容 */} </React.Fragment> ) }
在主页Index中用IndexPosts:
import { Suspense } from 'react' import IndexPosts from './IndexPosts' export default function Index() { return ( <main> <Suspense fallback={<div>加载中...</div>}> <IndexPosts /> </Suspense> {/* 其他页面内容 */} </main> ) }
2. 持久化缓存避免刷新丢失
页面刷新后React Query缓存默认清空,可通过持久化缓存到localStorage解决:
- 安装依赖:
@tanstack/react-query-persist-client @tanstack/query-sync-storage-persister - 修改入口文件配置:
import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { persistQueryClient } from '@tanstack/react-query-persist-client' import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5, // 5分钟内数据视为新鲜,避免重复请求 }, }, }) const persister = createSyncStoragePersister({ storage: window.localStorage, }) persistQueryClient({ queryClient, persister, })
3. 用startTransition包裹导航操作
确保路由跳转被标记为非紧急更新:
import { useNavigate, useLinkClickHandler } from 'react-router-dom' import { startTransition } from 'react' // 自定义Link组件 function CustomLink({ to, children }) { const navigate = useNavigate() const handleClick = useLinkClickHandler(to) const handleCustomClick = (e) => { startTransition(() => { handleClick(e) }) } return <a onClick={handleCustomClick}>{children}</a> }
4. 检查懒加载组件的Suspense包裹
栈追踪显示错误涉及Lazy组件,确保所有懒加载组件都在
const LazyComponent = React.lazy(() => import('./LazyComponent')) // 使用时 <Suspense fallback={<div>加载中...</div>}> <LazyComponent /> </Suspense>
内容的提问来源于stack exchange,提问作者user14695006
相关产品推荐
相关产品推荐

