From 675f9625f29895d1985fd71c849362c5a4385b6b Mon Sep 17 00:00:00 2001 From: Grady Hallenbeck Date: Fri, 6 Oct 2023 20:26:00 -0700 Subject: [PATCH] feat: migrate experimental app to use react data router --- src/RootApp.tsx | 25 +++++----- src/RootAppRouter.tsx | 17 +++++++ src/apps/experimental/App.tsx | 48 ------------------- src/apps/experimental/routes/routes.tsx | 42 +++++++++++++++++ src/components/router/AsyncRoute.tsx | 13 +++++ src/components/router/LegacyRoute.tsx | 7 +++ src/components/router/Redirect.tsx | 9 +++- src/hooks/useLegacyRouterSync.ts | 63 +++++++++++++++++++++++++ 8 files changed, 162 insertions(+), 62 deletions(-) create mode 100644 src/RootAppRouter.tsx delete mode 100644 src/apps/experimental/App.tsx create mode 100644 src/apps/experimental/routes/routes.tsx create mode 100644 src/hooks/useLegacyRouterSync.ts diff --git a/src/RootApp.tsx b/src/RootApp.tsx index cc10ca7baa..609cfc6fdd 100644 --- a/src/RootApp.tsx +++ b/src/RootApp.tsx @@ -4,42 +4,43 @@ import { History } from '@remix-run/router'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import React from 'react'; -import { useLocation } from 'react-router-dom'; import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App'; import AppHeader from 'components/AppHeader'; import Backdrop from 'components/Backdrop'; -import { HistoryRouter } from 'components/router/HistoryRouter'; import { ApiProvider } from 'hooks/useApi'; import { WebConfigProvider } from 'hooks/useWebConfig'; import theme from 'themes/theme'; +import { HistoryRouter } from 'components/router/HistoryRouter'; const DashboardApp = loadable(() => import('./apps/dashboard/App')); -const ExperimentalApp = loadable(() => import('./apps/experimental/App')); const StableApp = loadable(() => import('./apps/stable/App')); +const RootAppRouter = loadable(() => import('./RootAppRouter')); const queryClient = new QueryClient(); -const RootAppLayout = () => { +const RootAppLayout = ({ history }: { history: History }) => { const layoutMode = localStorage.getItem('layout'); const isExperimentalLayout = layoutMode === 'experimental'; - const location = useLocation(); const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS) - .some(path => location.pathname.startsWith(`/${path}`)); + .some(path => window.location.pathname.startsWith(`/${path}`)); return ( <> - { - isExperimentalLayout ? - : + {isExperimentalLayout ? + : + + } - + + + ); }; @@ -49,9 +50,7 @@ const RootApp = ({ history }: { history: History }) => ( - - - + diff --git a/src/RootAppRouter.tsx b/src/RootAppRouter.tsx new file mode 100644 index 0000000000..ff04d82545 --- /dev/null +++ b/src/RootAppRouter.tsx @@ -0,0 +1,17 @@ + +import { History } from '@remix-run/router'; +import React from 'react'; +import { RouterProvider, createHashRouter } from 'react-router-dom'; + +import { EXPERIMENTAL_APP_ROUTES } from 'apps/experimental/routes/routes'; +import { useLegacyRouterSync } from 'hooks/useLegacyRouterSync'; + +const router = createHashRouter([ + ...EXPERIMENTAL_APP_ROUTES +]); + +export default function RootAppRouter({ history }: { history: History}) { + useLegacyRouterSync({ router, history }); + + return ; +} diff --git a/src/apps/experimental/App.tsx b/src/apps/experimental/App.tsx deleted file mode 100644 index b17e9054ee..0000000000 --- a/src/apps/experimental/App.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import { Navigate, Route, Routes } from 'react-router-dom'; - -import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App'; -import { REDIRECTS } from 'apps/stable/routes/_redirects'; -import ConnectionRequired from 'components/ConnectionRequired'; -import { toAsyncPageRoute } from 'components/router/AsyncRoute'; -import { toViewManagerPageRoute } from 'components/router/LegacyRoute'; -import { toRedirectRoute } from 'components/router/Redirect'; - -import AppLayout from './AppLayout'; -import { ASYNC_USER_ROUTES } from './routes/asyncRoutes'; -import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes'; - -const ExperimentalApp = () => { - return ( - - }> - {/* User routes */} - }> - {ASYNC_USER_ROUTES.map(toAsyncPageRoute)} - {LEGACY_USER_ROUTES.map(toViewManagerPageRoute)} - - - {/* Public routes */} - }> - } /> - - {LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)} - - - - {/* Redirects for old paths */} - {REDIRECTS.map(toRedirectRoute)} - - {/* Ignore dashboard routes */} - {Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => ( - - ))} - - ); -}; - -export default ExperimentalApp; diff --git a/src/apps/experimental/routes/routes.tsx b/src/apps/experimental/routes/routes.tsx new file mode 100644 index 0000000000..09ef6d396f --- /dev/null +++ b/src/apps/experimental/routes/routes.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { RouteObject, redirect } from 'react-router-dom'; + +import { REDIRECTS } from 'apps/dashboard/routes/_redirects'; +import ConnectionRequired from 'components/ConnectionRequired'; +import { toAsyncPageRouteConfig } from 'components/router/AsyncRoute'; +import { toViewManagerPageRouteConfig } from 'components/router/LegacyRoute'; +import { toRedirectRouteConfig } from 'components/router/Redirect'; +import AppLayout from '../AppLayout'; +import { ASYNC_USER_ROUTES } from './asyncRoutes'; +import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes'; +import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App'; + +export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [ + { + path: '/*', + element: , + children: [ + { + /* User routes: Any child route of this layout is authenticated */ + element: , + children: [ + ...ASYNC_USER_ROUTES.map(toAsyncPageRouteConfig), + ...LEGACY_USER_ROUTES.map(toViewManagerPageRouteConfig) + ] + }, + + /* Public routes */ + { index: true, loader: () => redirect('/home.html') }, + ...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRouteConfig) + ] + }, + + /* Redirects for old paths */ + ...REDIRECTS.map(toRedirectRouteConfig), + + /* Ignore dashboard routes */ + ...Object.entries(DASHBOARD_APP_PATHS).map(([, path]) => ({ + path: `/${path}/*`, + element: null + })) +]; diff --git a/src/components/router/AsyncRoute.tsx b/src/components/router/AsyncRoute.tsx index 031e5700ea..aedd000867 100644 --- a/src/components/router/AsyncRoute.tsx +++ b/src/components/router/AsyncRoute.tsx @@ -52,3 +52,16 @@ export const toAsyncPageRoute = ({ path, page, element, type = AsyncRouteType.St /> ); }; + +export function toAsyncPageRouteConfig({ path, page, element, type = AsyncRouteType.Stable }: AsyncRoute) { + const Element = element || ( + type === AsyncRouteType.Experimental ? + ExperimentalAsyncPage : + StableAsyncPage + ); + + return { + path, + element: + }; +} diff --git a/src/components/router/LegacyRoute.tsx b/src/components/router/LegacyRoute.tsx index bba780a513..e0a9df7417 100644 --- a/src/components/router/LegacyRoute.tsx +++ b/src/components/router/LegacyRoute.tsx @@ -19,3 +19,10 @@ export function toViewManagerPageRoute(route: LegacyRoute) { /> ); } + +export function toViewManagerPageRouteConfig(route: LegacyRoute) { + return { + path: route.path, + element: + }; +} diff --git a/src/components/router/Redirect.tsx b/src/components/router/Redirect.tsx index 7354f16c55..188493242a 100644 --- a/src/components/router/Redirect.tsx +++ b/src/components/router/Redirect.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Navigate, Route, useLocation } from 'react-router-dom'; +import { Navigate, Route, RouteObject, useLocation } from 'react-router-dom'; export interface Redirect { from: string @@ -26,3 +26,10 @@ export function toRedirectRoute({ from, to }: Redirect) { /> ); } + +export function toRedirectRouteConfig({ from, to }: Redirect): RouteObject { + return { + path: from, + element: + }; +} diff --git a/src/hooks/useLegacyRouterSync.ts b/src/hooks/useLegacyRouterSync.ts new file mode 100644 index 0000000000..fe28df0905 --- /dev/null +++ b/src/hooks/useLegacyRouterSync.ts @@ -0,0 +1,63 @@ +import { Update } from 'history'; +import { useLayoutEffect, useState } from 'react'; +import type { History, Router } from '@remix-run/router'; + +const normalizePath = (pathname: string) => pathname.replace(/^!/, ''); + +interface UseLegacyRouterSyncProps { + router: Router; + history: History; +} +export function useLegacyRouterSync({ router, history }: UseLegacyRouterSyncProps) { + const [routerLocation, setRouterLocation] = useState(router.state.location); + + useLayoutEffect(() => { + const onHistoryChange = async (update: Update) => { + const isSynced = router.createHref(router.state.location) === router.createHref(update.location); + + /** + * Some legacy codepaths may still use the `#!` routing scheme which is unsupported with the React routing + * implementation, so we need to remove the leading `!` from the pathname. React Router already removes the + * hash for us. + */ + if (update.location.pathname.startsWith('!')) { + history.replace(normalizePath(update.location.pathname), update.location.state); + } else if (!isSynced) { + await router.navigate(update.location, { replace: true }); + } + }; + + const unlisten = history.listen(onHistoryChange); + + return () => { + unlisten(); + }; + }, [history, router]); + + /** + * Because the router subscription needs to be in a zero-dependencies effect, syncing changes to the router back to + * the legacy history API needs to be in a separate effect. This should run any time the router location changes. + */ + useLayoutEffect(() => { + const isSynced = router.createHref(routerLocation) === router.createHref(history.location); + if (!isSynced) { + history.replace(routerLocation); + } + }, [history, router, routerLocation]); + + /** + * We want to use an effect with no dependencies here when we set up the router subscription to ensure that we only + * subscribe to the router state once. The router doesn't provide a way to remove subscribers, so we need to be + * careful to not create multiple subscribers. + */ + useLayoutEffect(() => { + router.subscribe((newState) => { + setRouterLocation((prevLocation) => { + if (newState.location !== prevLocation) { + return newState.location; + } + return prevLocation; + }); + }); + }); +}