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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 01:24:30