You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 11:07:01