React Router V6懒加载嵌套路由报错:组件同步输入时挂起
React Router 6 路由懒加载与 Suspense 错误问题
我开发Web应用时采用react-router@6的RouterProvider构建路由系统,配置了包含嵌套路由的路由,所有路由(含嵌套路由)均实现懒加载。为解决页面切换时的闪烁问题,我使用了路由级代码拆分方案,通过React.Suspense将当前渲染页面作为fallback,避免切换时页面闪烁。但现在每次导航到任意URL都会报错:A component suspended while responding to synchronous input。
我尝试在以下位置包裹startTransition,但问题并未解决:
- FallbackProvider的
updateFallback方法 - Page组件的
useEffect回调 - use-page钩子的
onLoad函数
当我将use-page中的updateFallback(component)改为返回null时,错误消失,但也失去了所需的fallback效果(无法保留当前页面作为切换时的占位)。
index.tsx
import React from 'react'; import ReactDOM from 'react-dom/client'; import { Provider } from 'react-redux'; import App from './App'; import store from './store/app'; import './i18n/config'; import './styles/custom.scss'; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( <Provider store={store}> <App /> </Provider>, ); export default root;
App.tsx
import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import axios from 'axios'; import type { PayloadAction } from '@reduxjs/toolkit'; import type { IAutoAuthResponseData } from '@exlint.io/common'; import { backendApi, cliBackendApi } from './utils/http'; import type { IAuthPayload } from './store/interfaces/auth'; import type { AppState } from './store/app'; import { authActions } from './store/reducers/auth'; import AppView from './App.view'; interface IPropsFromState { readonly isAuthenticated: boolean | null; } interface IPropsFromDispatch { readonly auth: (loginPayload: IAuthPayload) => PayloadAction<IAuthPayload>; readonly setUnauthenticated: () => PayloadAction; } interface IProps extends IPropsFromState, IPropsFromDispatch {} const App: React.FC<IProps> = (props: React.PropsWithChildren<IProps>) => { return <AppView isAuthenticated={true} />; }; App.displayName = 'App'; App.defaultProps = {}; const mapStateToProps = (state: AppState) => { return { isAuthenticated: state.auth.isAuthenticated, }; }; export default connect(mapStateToProps, { auth: authActions.auth, setUnauthenticated: authActions.setUnauthenticated, })(React.memo(App));
App.view.tsx
import React, { useMemo } from 'react'; import { RouterProvider, createBrowserRouter } from 'react-router-dom'; import RouterBuilder from './App.router'; interface IProps { readonly isAuthenticated: boolean | null; } const AppView: React.FC<IProps> = (props: React.PropsWithChildren<IProps>) => { const routes = useMemo(() => { return RouterBuilder(props.isAuthenticated); }, [props.isAuthenticated]); return <RouterProvider router={createBrowserRouter(routes)} fallbackElement={null} />; }; AppView.displayName = 'AppView'; AppView.defaultProps = {}; export default React.memo(AppView);
App.router.tsx
import React from 'react'; import { Navigate, type RouteObject } from 'react-router-dom'; import AppLayout from './App.layout'; import { startProgress } from './services/progress-bar'; import { preloader } from './utils/http-backend'; const Auth = React.lazy(() => import('./pages/Auth')); const ExternalAuthRedirect = React.lazy(() => import('./pages/ExternalAuthRedirect')); const AccountSettings = React.lazy(() => import('./pages/AccountSettings')); const CliAuth = React.lazy(() => import('./pages/CliAuth')); const CliAuthenticated = React.lazy(() => import('./pages/CliAuthenticated')); const NotFound = React.lazy(() => import('./pages/NotFound')); const Account = React.lazy(() => import('@/containers/AccountSettings/Account')); const SecretManagement = React.lazy(() => import('@/containers/AccountSettings/SecretManagement')); const RouterBuilder = (isAuthenticated: boolean | null) => { const unAuthorizedRoutes: RouteObject[] = [ { path: '', element: <Auth />, }, { path: 'auth', element: <Auth />, }, { path: 'external-auth-redirect', element: <ExternalAuthRedirect />, }, ]; const authorizedRoutes: RouteObject[] = [ { path: 'account-settings', element: <AccountSettings />, children: [ { path: '', element: <Navigate to="account" replace />, }, { path: 'account', element: <Account />, }, { path: 'secret-management', element: <SecretManagement />, loader: async () => { startProgress(); await preloader('/user/secrets'); return null; }, }, { path: 'secret-management/new', element: <NewSecret />, }, { path: 'secret-management/*', element: <Navigate to="secret-management" replace />, }, { path: '*', element: <Navigate to="account" replace />, }, ], }, ]; const generalRoutes: RouteObject[] = [ { path: 'cli-auth', element: <CliAuth />, }, { path: 'cli-authenticated', element: <CliAuthenticated />, }, { path: 'not-found', element: <NotFound />, }, { path: '*', element: isAuthenticated === null ? null : <NotFound />, }, ]; const routes = [ { element: <AppLayout />, children: [...(isAuthenticated ? authorizedRoutes : unAuthorizedRoutes), ...generalRoutes], }, ]; return routes; }; export default RouterBuilder;
App.layout.tsx
import React from 'react'; import { Outlet } from 'react-router-dom'; import EDNotification from '@/ui/EDNotification'; import FallbackProvider from './helpers/FallbackProvider'; interface IProps {} const AppLayout: React.FC<IProps> = () => { return ( <FallbackProvider> <Outlet /> <div id="backdrop-root" /> <div id="overlay-root" /> <EDNotification /> </FallbackProvider> ); }; AppLayout.displayName = 'AppLayout'; AppLayout.defaultProps = {}; export default React.memo(AppLayout);
FallbackProvider.tsx
import React, { Suspense, useCallback, useMemo, useState } from 'react'; import { FallbackContext } from './context/fallback'; import type { FallbackType } from './interfaces/types'; interface IProps {} const FabllbackProvider: React.FC<IProps> = (props: React.PropsWithChildren<IProps>) => { const [fallbackState, setFallbackState] = useState<FallbackType>(null); const updateFallback = useCallback((fallback: FallbackType) => { setFallbackState(() => fallback); }, []); const renderChildren = useMemo(() => { return props.children; }, [props.children]); return ( <FallbackContext.Provider value={{ updateFallback }}> <Suspense fallback={fallbackState}>{renderChildren}</Suspense> </FallbackContext.Provider> ); }; FabllbackProvider.displayName = 'FabllbackProvider'; FabllbackProvider.defaultProps = {}; export default React.memo(FabllbackProvider);
FallbackContext
import { createContext } from 'react'; import type { FallbackContextType } from '../interfaces/types'; export const FallbackContext = createContext<FallbackContextType>({ updateFallback: () => { return; }, });
Page.tsx
import React, { useEffect, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; import { usePage } from '@/hooks/use-page'; import { endProgress, startProgress } from '@/services/progress-bar'; interface IProps {} const Page: React.FC<IProps> = (props: React.PropsWithChildren<IProps>) => { const { onLoad } = usePage(); const location = useLocation(); const render = useMemo(() => { // eslint-disable-next-line react/jsx-no-useless-fragment return <>{props.children}</>; }, [props.children]); useEffect(() => { onLoad(render); }, [onLoad, render]); useEffect(() => { endProgress(); return () => startProgress(); }, [location]); return render; }; Page.displayName = 'Page'; Page.defaultProps = {}; export default Page;
use-page.ts
import { useCallback, useContext } from 'react'; import type { FallbackType } from '../helpers/FallbackProvider/interfaces/types'; import { FallbackContext } from '../helpers/FallbackProvider/context/fallback'; export const usePage = () => { const { updateFallback } = useContext(FallbackContext); const onLoad = useCallback( (component: FallbackType | undefined) => { if (component === undefined) { component = null; } updateFallback(component); }, [updateFallback], ); return { onLoad }; };
页面示例(AuthPage)
import React from 'react'; import Auth from '@/containers/Auth'; import Page from '@/helpers/Page'; interface IProps {} const AuthPage: React.FC<IProps> = () => { return ( <Page> <Auth /> </Page> ); }; AuthPage.displayName = 'AuthPage'; AuthPage.defaultProps = {}; export default AuthPage;
progress-bar.ts
import nProgress from 'nprogress'; nProgress.configure({ showSpinner: false, }); export const startProgress = () => { nProgress.start(); }; export const endProgress = () => { nProgress.done(); };
内容的提问来源于stack exchange,提问作者Tal Rofe
相关产品推荐
相关产品推荐

