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