diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 05b512b8aea..7b5e4aeccd6 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -71,6 +71,7 @@
- [scampower3](https://github.com/scampower3)
- [LittleBigOwI] (https://github.com/LittleBigOwI/)
- [Nate G](https://github.com/GGProGaming)
+ - [Grady Hallenbeck](https://github.com/grhallenbeck)
# Emby Contributors
diff --git a/src/RootApp.tsx b/src/RootApp.tsx
index cc10ca7baa5..23ff1bb2303 100644
--- a/src/RootApp.tsx
+++ b/src/RootApp.tsx
@@ -4,59 +4,35 @@ 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';
-const DashboardApp = loadable(() => import('./apps/dashboard/App'));
-const ExperimentalApp = loadable(() => import('./apps/experimental/App'));
-const StableApp = loadable(() => import('./apps/stable/App'));
+const StableAppRouter = loadable(() => import('./apps/stable/AppRouter'));
+const RootAppRouter = loadable(() => import('./RootAppRouter'));
const queryClient = new QueryClient();
-const RootAppLayout = () => {
+const RootApp = ({ history }: Readonly<{ 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}`));
-
return (
- <>
-
-
-
- {
- isExperimentalLayout ?
- :
-
- }
-
-
- >
+
+
+
+
+ {isExperimentalLayout ?
+ :
+
+ }
+
+
+
+
+
);
};
-const RootApp = ({ history }: { history: History }) => (
-
-
-
-
-
-
-
-
-
-
-
-
-);
-
export default RootApp;
diff --git a/src/RootAppRouter.tsx b/src/RootAppRouter.tsx
new file mode 100644
index 00000000000..9a15264db00
--- /dev/null
+++ b/src/RootAppRouter.tsx
@@ -0,0 +1,41 @@
+
+import { History } from '@remix-run/router';
+import React from 'react';
+import {
+ RouterProvider,
+ createHashRouter,
+ Outlet
+} from 'react-router-dom';
+
+import { EXPERIMENTAL_APP_ROUTES } from 'apps/experimental/routes/routes';
+import AppHeader from 'components/AppHeader';
+import Backdrop from 'components/Backdrop';
+import { useLegacyRouterSync } from 'hooks/useLegacyRouterSync';
+import { DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
+
+const router = createHashRouter([
+ {
+ element: ,
+ children: [
+ ...EXPERIMENTAL_APP_ROUTES,
+ ...DASHBOARD_APP_ROUTES
+ ]
+ }
+]);
+
+export default function RootAppRouter({ history }: Readonly<{ history: History}>) {
+ useLegacyRouterSync({ router, history });
+
+ return ;
+}
+
+function RootAppLayout() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/src/apps/dashboard/App.tsx b/src/apps/dashboard/App.tsx
deleted file mode 100644
index 640a60d1f80..00000000000
--- a/src/apps/dashboard/App.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import loadable from '@loadable/component';
-import React from 'react';
-import { Route, Routes } from 'react-router-dom';
-
-import ConnectionRequired from 'components/ConnectionRequired';
-import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
-import { AsyncPageProps, AsyncRoute, toAsyncPageRoute } from 'components/router/AsyncRoute';
-import { toRedirectRoute } from 'components/router/Redirect';
-import ServerContentPage from 'components/ServerContentPage';
-
-import AppLayout from './AppLayout';
-import { REDIRECTS } from './routes/_redirects';
-import { ASYNC_ADMIN_ROUTES } from './routes/_asyncRoutes';
-import { LEGACY_ADMIN_ROUTES } from './routes/_legacyRoutes';
-
-const DashboardAsyncPage = loadable(
- (props: { page: string }) => import(/* webpackChunkName: "[request]" */ `./routes/${props.page}`),
- { cacheKey: (props: AsyncPageProps) => props.page }
-);
-
-const toDashboardAsyncPageRoute = (route: AsyncRoute) => (
- toAsyncPageRoute({
- ...route,
- element: DashboardAsyncPage
- })
-);
-
-export const DASHBOARD_APP_PATHS = {
- Dashboard: 'dashboard',
- MetadataManager: 'metadata',
- PluginConfig: 'configurationpage'
-};
-
-const DashboardApp = () => (
-
- }>
- }>
-
- {ASYNC_ADMIN_ROUTES.map(toDashboardAsyncPageRoute)}
- {LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
-
-
- {/* NOTE: The metadata editor might deserve a dedicated app in the future */}
- {toViewManagerPageRoute({
- path: DASHBOARD_APP_PATHS.MetadataManager,
- pageProps: {
- controller: 'edititemmetadata',
- view: 'edititemmetadata.html'
- }
- })}
-
-
- } />
-
-
-
- {/* Suppress warnings for unhandled routes */}
-
-
- {/* Redirects for old paths */}
- {REDIRECTS.map(toRedirectRoute)}
-
-);
-
-export default DashboardApp;
diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts
index 09d40de0e83..9abd6b75e32 100644
--- a/src/apps/dashboard/routes/_asyncRoutes.ts
+++ b/src/apps/dashboard/routes/_asyncRoutes.ts
@@ -1,12 +1,12 @@
-import type { AsyncRoute } from 'components/router/AsyncRoute';
+import { AsyncRouteType, type AsyncRoute } from 'components/router/AsyncRoute';
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
- { path: 'activity' },
- { path: 'notifications' },
- { path: 'users' },
- { path: 'users/access' },
- { path: 'users/add' },
- { path: 'users/parentalcontrol' },
- { path: 'users/password' },
- { path: 'users/profile' }
+ { path: 'activity', type: AsyncRouteType.Dashboard },
+ { path: 'notifications', type: AsyncRouteType.Dashboard },
+ { path: 'users', type: AsyncRouteType.Dashboard },
+ { path: 'users/access', type: AsyncRouteType.Dashboard },
+ { path: 'users/add', type: AsyncRouteType.Dashboard },
+ { path: 'users/parentalcontrol', type: AsyncRouteType.Dashboard },
+ { path: 'users/password', type: AsyncRouteType.Dashboard },
+ { path: 'users/profile', type: AsyncRouteType.Dashboard }
];
diff --git a/src/apps/dashboard/routes/routes.tsx b/src/apps/dashboard/routes/routes.tsx
new file mode 100644
index 00000000000..7bbeb3fc4e0
--- /dev/null
+++ b/src/apps/dashboard/routes/routes.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import { RouteObject } from 'react-router-dom';
+import AppLayout from '../AppLayout';
+import ConnectionRequired from 'components/ConnectionRequired';
+import { ASYNC_ADMIN_ROUTES } from './_asyncRoutes';
+import { toAsyncPageRoute } from 'components/router/AsyncRoute';
+import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
+import { LEGACY_ADMIN_ROUTES } from './_legacyRoutes';
+import ServerContentPage from 'components/ServerContentPage';
+
+export const DASHBOARD_APP_PATHS = {
+ Dashboard: 'dashboard',
+ MetadataManager: 'metadata',
+ PluginConfig: 'configurationpage'
+};
+
+export const DASHBOARD_APP_ROUTES: RouteObject[] = [
+ {
+ element: ,
+ children: [
+ {
+ element: ,
+ children: [
+ {
+ path: DASHBOARD_APP_PATHS.Dashboard,
+ children: [
+ ...ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute),
+ ...LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)
+ ]
+ },
+
+ /* NOTE: The metadata editor might deserve a dedicated app in the future */
+ toViewManagerPageRoute({
+ path: DASHBOARD_APP_PATHS.MetadataManager,
+ pageProps: {
+ controller: 'edititemmetadata',
+ view: 'edititemmetadata.html'
+ }
+ }),
+
+ {
+ path: DASHBOARD_APP_PATHS.PluginConfig,
+ element:
+ }
+ ]
+ }
+ ]
+ }
+];
diff --git a/src/apps/experimental/App.tsx b/src/apps/experimental/App.tsx
deleted file mode 100644
index b17e9054ee6..00000000000
--- 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 00000000000..3156f5f0e33
--- /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 { DASHBOARD_APP_PATHS } from 'apps/dashboard/routes/routes';
+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 './asyncRoutes';
+import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
+
+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(toAsyncPageRoute),
+ ...LEGACY_USER_ROUTES.map(toViewManagerPageRoute)
+ ]
+ },
+
+ /* Public routes */
+ { index: true, loader: () => redirect('/home.html') },
+ ...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)
+ ]
+ },
+
+ /* Redirects for old paths */
+ ...REDIRECTS.map(toRedirectRoute),
+
+ /* Ignore dashboard routes */
+ ...Object.entries(DASHBOARD_APP_PATHS).map(([, path]) => ({
+ path: `/${path}/*`,
+ element: null
+ }))
+];
diff --git a/src/apps/stable/App.tsx b/src/apps/stable/App.tsx
deleted file mode 100644
index 9b0adbab9cc..00000000000
--- a/src/apps/stable/App.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import React from 'react';
-import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
-
-import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
-import AppBody from 'components/AppBody';
-import ConnectionRequired from 'components/ConnectionRequired';
-import { toAsyncPageRoute } from 'components/router/AsyncRoute';
-import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
-import { toRedirectRoute } from 'components/router/Redirect';
-
-import { ASYNC_USER_ROUTES } from './routes/asyncRoutes';
-import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
-import { REDIRECTS } from './routes/_redirects';
-
-const Layout = () => (
-
-
-
-);
-
-const StableApp = () => (
-
- }>
- {/* User routes */}
- }>
- {ASYNC_USER_ROUTES.map(toAsyncPageRoute)}
- {LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
-
-
- {/* Public routes */}
- }>
- } />
-
- {LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)}
-
-
- {/* Suppress warnings for unhandled routes */}
-
-
-
- {/* Redirects for old paths */}
- {REDIRECTS.map(toRedirectRoute)}
-
- {/* Ignore dashboard routes */}
- {Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => (
-
- ))}
-
-);
-
-export default StableApp;
diff --git a/src/apps/stable/AppLayout.tsx b/src/apps/stable/AppLayout.tsx
new file mode 100644
index 00000000000..bac962784c8
--- /dev/null
+++ b/src/apps/stable/AppLayout.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { Outlet, useLocation } from 'react-router-dom';
+
+import AppBody from 'components/AppBody';
+import { DASHBOARD_APP_PATHS } from 'apps/dashboard/routes/routes';
+import Backdrop from 'components/Backdrop';
+import AppHeader from 'components/AppHeader';
+
+export default function AppLayout() {
+ const location = useLocation();
+ const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
+ .some(path => location.pathname.startsWith(`/${path}`));
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/apps/stable/AppRouter.tsx b/src/apps/stable/AppRouter.tsx
new file mode 100644
index 00000000000..761ade98a86
--- /dev/null
+++ b/src/apps/stable/AppRouter.tsx
@@ -0,0 +1,18 @@
+import { History } from '@remix-run/router';
+import React from 'react';
+import { RouterProvider, createHashRouter } from 'react-router-dom';
+
+import { DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
+import { useLegacyRouterSync } from 'hooks/useLegacyRouterSync';
+import { STABLE_APP_ROUTES } from './routes/routes';
+
+const router = createHashRouter([
+ ...STABLE_APP_ROUTES,
+ ...DASHBOARD_APP_ROUTES
+]);
+
+export default function StableAppRouter({ history }: Readonly<{ history: History }>) {
+ useLegacyRouterSync({ router, history });
+
+ return ;
+}
diff --git a/src/apps/stable/routes/routes.tsx b/src/apps/stable/routes/routes.tsx
new file mode 100644
index 00000000000..77f936276f6
--- /dev/null
+++ b/src/apps/stable/routes/routes.tsx
@@ -0,0 +1,46 @@
+import { RouteObject, redirect } from 'react-router-dom';
+import React from 'react';
+
+import { DASHBOARD_APP_PATHS } from 'apps/dashboard/routes/routes';
+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 { REDIRECTS } from './_redirects';
+import { ASYNC_USER_ROUTES } from './asyncRoutes';
+import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
+
+export const STABLE_APP_ROUTES: RouteObject[] = [
+ {
+ path: '/*',
+ element: ,
+ children: [
+ {
+ /* User routes */
+ element: ,
+ children: [
+ ...ASYNC_USER_ROUTES.map(toAsyncPageRoute),
+ ...LEGACY_USER_ROUTES.map(toViewManagerPageRoute)
+ ]
+ },
+
+ /* Public routes */
+ { index: true, loader: () => redirect('/home.html') },
+ ...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute),
+
+ /* Suppress warnings for unhandled routes */
+ { path: '*', element: null }
+ ]
+ },
+
+ /* Redirects for old paths */
+ ...REDIRECTS.map(toRedirectRoute),
+
+ /* 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 031e5700eaf..19457851d31 100644
--- a/src/components/router/AsyncRoute.tsx
+++ b/src/components/router/AsyncRoute.tsx
@@ -1,10 +1,10 @@
import loadable, { LoadableComponent } from '@loadable/component';
import React from 'react';
-import { Route } from 'react-router-dom';
export enum AsyncRouteType {
Stable,
- Experimental
+ Experimental,
+ Dashboard,
}
export interface AsyncRoute {
@@ -26,6 +26,11 @@ export interface AsyncPageProps {
page: string
}
+const DashboardAsyncPage = loadable(
+ (props: { page: string }) => import(/* webpackChunkName: "[request]" */ `../../apps/dashboard/routes/${props.page}`),
+ { cacheKey: (props: AsyncPageProps) => props.page }
+);
+
const ExperimentalAsyncPage = loadable(
(props: { page: string }) => import(/* webpackChunkName: "[request]" */ `../../apps/experimental/routes/${props.page}`),
{ cacheKey: (props: AsyncPageProps) => props.page }
@@ -36,19 +41,24 @@ const StableAsyncPage = loadable(
{ cacheKey: (props: AsyncPageProps) => props.page }
);
-export const toAsyncPageRoute = ({ path, page, element, type = AsyncRouteType.Stable }: AsyncRoute) => {
- const Element = element
- || (
- type === AsyncRouteType.Experimental ?
- ExperimentalAsyncPage :
- StableAsyncPage
- );
-
- return (
- }
- />
- );
-};
+export function toAsyncPageRoute({ path, page, element, type = AsyncRouteType.Stable }: AsyncRoute) {
+ let Element = element;
+ if (!Element) {
+ switch (type) {
+ case AsyncRouteType.Dashboard:
+ Element = DashboardAsyncPage;
+ break;
+ case AsyncRouteType.Experimental:
+ Element = ExperimentalAsyncPage;
+ break;
+ case AsyncRouteType.Stable:
+ default:
+ Element = StableAsyncPage;
+ }
+ }
+
+ return {
+ path,
+ element:
+ };
+}
diff --git a/src/components/router/HistoryRouter.tsx b/src/components/router/HistoryRouter.tsx
deleted file mode 100644
index 21e1efe0fed..00000000000
--- a/src/components/router/HistoryRouter.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import React, { useLayoutEffect } from 'react';
-import { HistoryRouterProps, Router } from 'react-router-dom';
-import { Update } from 'history';
-
-/** Strips leading "!" from paths */
-const normalizePath = (pathname: string) => pathname.replace(/^!/, '');
-
-/**
- * A slightly customized version of the HistoryRouter from react-router-dom.
- * We need to use HistoryRouter to have a shared history state between react-router and appRouter, but it does not seem
- * to be properly exported in the upstream package.
- * We also needed some customizations to handle #! routes.
- * Refs: https://github.com/remix-run/react-router/blob/v6.3.0/packages/react-router-dom/index.tsx#L222
- */
-export function HistoryRouter({ basename, children, history }: HistoryRouterProps) {
- const [state, setState] = React.useState({
- action: history.action,
- location: history.location
- });
-
- useLayoutEffect(() => {
- const onHistoryChange = (update: Update) => {
- if (update.location.pathname.startsWith('!')) {
- // When the location changes, we need to check for #! paths and replace the location with the "!" stripped
- history.replace(normalizePath(update.location.pathname), update.location.state);
- } else {
- setState(update);
- }
- };
-
- history.listen(onHistoryChange);
- }, [ history ]);
-
- return (
-
- );
-}
diff --git a/src/components/router/LegacyRoute.tsx b/src/components/router/LegacyRoute.tsx
index bba780a513a..983c4d84cfb 100644
--- a/src/components/router/LegacyRoute.tsx
+++ b/src/components/router/LegacyRoute.tsx
@@ -1,5 +1,4 @@
import React from 'react';
-import { Route } from 'react-router-dom';
import ViewManagerPage, { ViewManagerPageProps } from '../viewManager/ViewManagerPage';
@@ -9,13 +8,8 @@ export interface LegacyRoute {
}
export function toViewManagerPageRoute(route: LegacyRoute) {
- return (
-
- }
- />
- );
+ return {
+ path: route.path,
+ element:
+ };
}
diff --git a/src/components/router/Redirect.tsx b/src/components/router/Redirect.tsx
index 7354f16c554..d68ecdea79f 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, RouteObject, useLocation } from 'react-router-dom';
export interface Redirect {
from: string
@@ -17,12 +17,9 @@ const RedirectWithSearch = ({ to }: { to: string }) => {
);
};
-export function toRedirectRoute({ from, to }: Redirect) {
- return (
- }
- />
- );
+export function toRedirectRoute({ 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 00000000000..fe28df09059
--- /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;
+ });
+ });
+ });
+}