import { Update } from 'history'; import { useLayoutEffect, useState } from 'react'; import type { History, Router } from '@remix-run/router'; 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( { ...update.location, pathname: update.location.pathname.replace(/^\/!/, '') }, update.location.state); } else if (update.location.pathname.startsWith('/!')) { history.replace( { ...update.location, pathname: update.location.pathname.replace(/^\/!/, '/') }, update.location.state); } else if (update.location.pathname.startsWith('!')) { history.replace( { ...update.location, pathname: update.location.pathname.replace(/^!/, '') }, 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; }); }); }); }