javascript 延迟加载,React Router V6和嵌套路由收到错误:“响应同步输入时挂起的组件.”

xytpbqjk  于 2023-03-06  发布在  Java
关注(0)|答案(1)|浏览(156)
    • bounty已结束**。此问题的答案可获得+150声望奖励。奖励宽限期将在23小时后结束。Tal Rofe正在寻找规范答案

我的用例如下所示:我构建了一个Web应用程序,并使用react-router@6构建应用程序的路由。我使用的是react-router-dom中的RouterProvider。然后,我配置了一些路由,以及嵌套路由。每个路由,包括嵌套路由,都是延迟加载的。我现在遇到的问题是,当我从一个页面导航到另一个页面时,应用程序屏幕刷新。为了解决这个问题,我使用了这个技巧:https://github.com/HanMoeHtet/route-level-code-split
简而言之,我使用React.Suspense和当前呈现页面的fallback值,这样,当我导航到其他页面(延迟加载)时,fallback元素就是我导航的那个页面,这就是屏幕不再刷新的原因。
我将在这里提供我的全部代码,但大多数代码块都将从https://github.com/HanMoeHtet/route-level-code-split复制,因此如果需要,您可以在这里查看
下面是应用程序的入口文件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);

这是相关的背景:

import { createContext } from 'react';

import type { FallbackContextType } from '../interfaces/types';

export const FallbackContext = createContext<FallbackContextType>({
    updateFallback: () => {
        return;
    },
});

然后我得到了这个Page.tsx Package 器:

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 };
};

现在,对于App.router.tsx中的每个页面(每个页面从pages文件夹加载,而不是components文件夹!),我做以下事情:

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();
};

然后我尝试加载我的网站,但我得到这个错误:

Unexpected Application 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@http://localhost:8080/node_modules/.vite/deps/chunk-LCEHA6P5.js?v=0485a108:14217:43
handleError@http://localhost:8080/node_modules/.vite/deps/chunk-LCEHA6P5.js?v=0485a108:19030:29
renderRootSync@http://localhost:8080/node_modules/.vite/deps/chunk-LCEHA6P5.js?v=0485a108:19115:26
recoverFromConcurrentError@http://localhost:8080/node_modules/.vite/deps/chunk-LCEHA6P5.js?v=0485a108:18732:42
performSyncWorkOnRoot@http://localhost:8080/node_modules/.vite/deps/chunk-LCEHA6P5.js?v=0485a108:18875:28
flushSyncCallbacks@http://localhost:8080/node_modules/.vite/deps/chunk-LCEHA6P5.js?v=0485a108:9135:30
../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.development.js/ensureRootIsScheduled/<@http://localhost:8080/node_modules/.vite/deps/chunk-LCEHA6P5.js?v=0485a108:18623:21

💿 Hey developer 👋

You can provide a way better UX than this when your app throws errors by providing your own errorElement props on <Route>

每次我导航到网站中的任何URL时都会发生这种情况。
第一次尝试
我在与FallbackProvider.tsx文件相关的几个地方开始使用startTransition functionb,在这一行:<Suspense fallback={fallbackState}>{renderChildren}</Suspense>.
我就是这么做的:
FallbackProvider.tsx中,我将updateFallback函数更改为:

const updateFallback = useCallback((fallback: FallbackType) => {
        startTransition(() => {
            setFallbackState(() => fallback);
        });
    }, []);

Page.tsx中,我将2 useEffect修改为:

useEffect(() => {
        startTransition(() => {
            onLoad(render);
        });
    }, [onLoad, render]);

    useEffect(() => {
        startTransition(() => {
            endProgress();
        });

        return () => startProgress();
    }, [location]);

use-page.ts文件中,我将onLoad函数修改为:

const onLoad = useCallback(
        (component: FallbackType | undefined) => {
            if (component === undefined) {
                component = null;
            }

            startTransition(() => {
                updateFallback(component!);
            });
        },
        [updateFallback],
    );

这些变化并没有帮助我,错误是一样的。

更新问题

我更改了use-page.ts文件中的这行代码:updateFallback(component);return null;,这个问题就不会再出现了。但是当然..现在我取消了代码的要点(现在当我返回null时,不会有我想要的后备-渲染器页面)。

wooyq4lh

wooyq4lh1#

我不知道你的问题到底是什么,但既然你有一个问题,悬念这里是如何工作的:当您转到某个页面时,例如,当我们第一次启动应用程序时,它会打开主页(又名'/'页面),如果您使用Suspense,它只会尝试下载此页面的内容,这种方式加载速度更快,因为它不会加载任何不必要的代码,当您切换到另一个页面时,它会缓存下载的内容并尝试下载下一个页面,例如,假设我们尝试进入'/about'页面,然后它会尝试下载与About页面相关的代码,每次您更改页面时,它都会下载更多代码,并且在代码下载过程中,它会显示Suspense的回退。
但是如果你有嵌套的路由,并且你不想暂停整个应用程序,你可以将嵌套的路由 Package 在另一个暂停中。
下面是一个代码示例:

const authorizedRoutes: RouteObject[] = [
        {
            path: 'account-settings',
            element: <Suspense fallback='nested fallback'> //this way when you change pages between Account or SecretManagement, etc... it'll render the nested fallback instead of you other fallback 
                       <AccountSettings />
                     </Suspense>,
            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 />,
                },
            ],
        },
    ];

你也可以为你的其他路线这样做。

相关问题