import {
ReactNode,
useCallback,
useEffect,
useMemo,
useState,
useTransition,
} from 'react';
import { RouteGlobalProvider, RouteLevelProvider } from './context.js';
import { PreviousLocation, RouteState, useOnLocationChange } from './hooks.js';
import { Outlet } from './Outlet.js';
import { RouteConfig, RouteMatch } from './types.js';
import { joinPaths, removeBasePath } from './util.js';
export interface RouterProps {
children: ReactNode;
routes: RouteConfig[];
onNavigate?: (
location: Location,
routerState: {
state?: any;
skipTransition?: boolean;
},
previous: PreviousLocation,
) => boolean | void;
/**
* Pass this to re-enable browser-native scroll restoration
* instead of manually restoring scroll positions with the
* RestoreScroll component. i.e. if you are not using RestoreScroll.
*/
nativeScrollRestoration?: boolean;
basePath?: string;
}
const Root = () => ;
export function Router({
children,
routes,
onNavigate,
nativeScrollRestoration,
basePath,
}: RouterProps) {
const rootRoute = useMemo(
() => ({
path: '',
children: routes,
component: Root,
}),
[routes],
);
const [path, setPath] = useState(() =>
removeBasePath(window.location.pathname, basePath),
);
const root: RouteMatch = useMemo(() => {
const adjustedPath =
basePath && path.startsWith(basePath)
? path.slice(basePath.length)
: path;
return {
path: basePath && adjustedPath !== path ? basePath : '',
remainingPath: adjustedPath,
params: {},
route: rootRoute,
};
}, [rootRoute, path, basePath]);
const [transitioning, startTransition] = useTransition();
const [events] = useState(() => new EventTarget());
const updatePath = useCallback(
(newPath: string, state?: RouteState) => {
if (state?.skipTransition) {
setPath(newPath);
} else {
startTransition(() => {
setPath(newPath);
});
}
},
[setPath, startTransition],
);
useEffect(() => {
if (nativeScrollRestoration) return;
window.history.scrollRestoration = 'manual';
return () => {
window.history.scrollRestoration = 'auto';
};
}, [nativeScrollRestoration]);
// enforce base path
useEffect(() => {
if (basePath && !window.location.pathname.startsWith(basePath)) {
window.history.replaceState(
{},
'',
joinPaths(basePath, window.location.pathname) + window.location.search,
);
}
}, [basePath]);
return (
{children}
);
}
function PathManager({
children,
setPath,
onNavigate,
}: {
children: ReactNode;
setPath: (path: string, state?: RouteState) => void;
onNavigate?: RouterProps['onNavigate'];
}) {
useOnLocationChange((location, state, previous) => {
const cancelNavigation = onNavigate?.(location, state, previous) === false;
if (cancelNavigation) return;
setPath(location.pathname, state);
});
return <>{children}>;
}