/** * NOTE: If you refactor this to split up the modules into separate files, * you'll need to update the rollup config for react-router-dom-v5-compat. */ import * as React from "react"; import * as ReactDOM from "react-dom"; import type { DataRouteObject, FutureConfig, Location, NavigateOptions, NavigationType, Navigator, RelativeRoutingType, RouteObject, RouterProviderProps, To, } from "react-router"; import { Router, createPath, useHref, useLocation, useMatches, useNavigate, useNavigation, useResolvedPath, useBlocker, UNSAFE_DataRouterContext as DataRouterContext, UNSAFE_DataRouterStateContext as DataRouterStateContext, UNSAFE_NavigationContext as NavigationContext, UNSAFE_RouteContext as RouteContext, UNSAFE_mapRouteProperties as mapRouteProperties, UNSAFE_useRouteId as useRouteId, UNSAFE_useRoutesImpl as useRoutesImpl, } from "react-router"; import type { BrowserHistory, unstable_DataStrategyFunction, unstable_DataStrategyFunctionArgs, unstable_DataStrategyMatch, Fetcher, FormEncType, FormMethod, FutureConfig as RouterFutureConfig, GetScrollRestorationKeyFunction, HashHistory, History, HTMLFormMethod, HydrationState, Router as RemixRouter, V7_FormMethod, RouterState, RouterSubscriber, BlockerFunction, } from "@remix-run/router"; import { createRouter, createBrowserHistory, createHashHistory, joinPaths, stripBasename, UNSAFE_ErrorResponseImpl as ErrorResponseImpl, UNSAFE_invariant as invariant, UNSAFE_warning as warning, matchPath, IDLE_FETCHER, } from "@remix-run/router"; import type { SubmitOptions, ParamKeyValuePair, URLSearchParamsInit, SubmitTarget, } from "./dom"; import { createSearchParams, defaultMethod, getFormSubmissionInfo, getSearchParamsForLocation, shouldProcessLinkClick, } from "./dom"; //////////////////////////////////////////////////////////////////////////////// //#region Re-exports //////////////////////////////////////////////////////////////////////////////// export type { unstable_DataStrategyFunction, unstable_DataStrategyFunctionArgs, unstable_DataStrategyMatch, FormEncType, FormMethod, GetScrollRestorationKeyFunction, ParamKeyValuePair, SubmitOptions, URLSearchParamsInit, V7_FormMethod, }; export { createSearchParams, ErrorResponseImpl as UNSAFE_ErrorResponseImpl }; // Note: Keep in sync with react-router exports! export type { ActionFunction, ActionFunctionArgs, AwaitProps, Blocker, BlockerFunction, DataRouteMatch, DataRouteObject, ErrorResponse, Fetcher, FutureConfig, Hash, IndexRouteObject, IndexRouteProps, JsonFunction, LazyRouteFunction, LayoutRouteProps, LoaderFunction, LoaderFunctionArgs, Location, MemoryRouterProps, NavigateFunction, NavigateOptions, NavigateProps, Navigation, Navigator, NonIndexRouteObject, OutletProps, Params, ParamParseKey, Path, PathMatch, Pathname, PathParam, PathPattern, PathRouteProps, RedirectFunction, RelativeRoutingType, RouteMatch, RouteObject, RouteProps, RouterProps, RouterProviderProps, RoutesProps, Search, ShouldRevalidateFunction, ShouldRevalidateFunctionArgs, To, UIMatch, unstable_HandlerResult, } from "react-router"; export { AbortedDeferredError, Await, MemoryRouter, Navigate, NavigationType, Outlet, Route, Router, Routes, createMemoryRouter, createPath, createRoutesFromChildren, createRoutesFromElements, defer, isRouteErrorResponse, generatePath, json, matchPath, matchRoutes, parsePath, redirect, redirectDocument, renderMatches, resolvePath, useActionData, useAsyncError, useAsyncValue, useBlocker, useHref, useInRouterContext, useLoaderData, useLocation, useMatch, useMatches, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRevalidator, useRouteError, useRouteLoaderData, useRoutes, } from "react-router"; /////////////////////////////////////////////////////////////////////////////// // DANGER! PLEASE READ ME! // We provide these exports as an escape hatch in the event that you need any // routing data that we don't provide an explicit API for. With that said, we // want to cover your use case if we can, so if you feel the need to use these // we want to hear from you. Let us know what you're building and we'll do our // best to make sure we can support you! // // We consider these exports an implementation detail and do not guarantee // against any breaking changes, regardless of the semver release. Use with // extreme caution and only if you understand the consequences. Godspeed. /////////////////////////////////////////////////////////////////////////////// /** @internal */ export { UNSAFE_DataRouterContext, UNSAFE_DataRouterStateContext, UNSAFE_NavigationContext, UNSAFE_LocationContext, UNSAFE_RouteContext, UNSAFE_useRouteId, } from "react-router"; //#endregion declare global { var __staticRouterHydrationData: HydrationState | undefined; var __reactRouterVersion: string; interface Document { startViewTransition(cb: () => Promise | void): ViewTransition; } } // HEY YOU! DON'T TOUCH THIS VARIABLE! // // It is replaced with the proper version at build time via a babel plugin in // the rollup config. // // Export a global property onto the window for React Router detection by the // Core Web Vitals Technology Report. This way they can configure the `wappalyzer` // to detect and properly classify live websites as being built with React Router: // https://github.com/HTTPArchive/wappalyzer/blob/main/src/technologies/r.json const REACT_ROUTER_VERSION = "0"; try { window.__reactRouterVersion = REACT_ROUTER_VERSION; } catch (e) { // no-op } //////////////////////////////////////////////////////////////////////////////// //#region Routers //////////////////////////////////////////////////////////////////////////////// interface DOMRouterOpts { basename?: string; future?: Partial>; hydrationData?: HydrationState; unstable_dataStrategy?: unstable_DataStrategyFunction; window?: Window; } export function createBrowserRouter( routes: RouteObject[], opts?: DOMRouterOpts ): RemixRouter { return createRouter({ basename: opts?.basename, future: { ...opts?.future, v7_prependBasename: true, }, history: createBrowserHistory({ window: opts?.window }), hydrationData: opts?.hydrationData || parseHydrationData(), routes, mapRouteProperties, unstable_dataStrategy: opts?.unstable_dataStrategy, window: opts?.window, }).initialize(); } export function createHashRouter( routes: RouteObject[], opts?: DOMRouterOpts ): RemixRouter { return createRouter({ basename: opts?.basename, future: { ...opts?.future, v7_prependBasename: true, }, history: createHashHistory({ window: opts?.window }), hydrationData: opts?.hydrationData || parseHydrationData(), routes, mapRouteProperties, unstable_dataStrategy: opts?.unstable_dataStrategy, window: opts?.window, }).initialize(); } function parseHydrationData(): HydrationState | undefined { let state = window?.__staticRouterHydrationData; if (state && state.errors) { state = { ...state, errors: deserializeErrors(state.errors), }; } return state; } function deserializeErrors( errors: RemixRouter["state"]["errors"] ): RemixRouter["state"]["errors"] { if (!errors) return null; let entries = Object.entries(errors); let serialized: RemixRouter["state"]["errors"] = {}; for (let [key, val] of entries) { // Hey you! If you change this, please change the corresponding logic in // serializeErrors in react-router-dom/server.tsx :) if (val && val.__type === "RouteErrorResponse") { serialized[key] = new ErrorResponseImpl( val.status, val.statusText, val.data, val.internal === true ); } else if (val && val.__type === "Error") { // Attempt to reconstruct the right type of Error (i.e., ReferenceError) if (val.__subType) { let ErrorConstructor = window[val.__subType]; if (typeof ErrorConstructor === "function") { try { // @ts-expect-error let error = new ErrorConstructor(val.message); // Wipe away the client-side stack trace. Nothing to fill it in with // because we don't serialize SSR stack traces for security reasons error.stack = ""; serialized[key] = error; } catch (e) { // no-op - fall through and create a normal Error } } } if (serialized[key] == null) { let error = new Error(val.message); // Wipe away the client-side stack trace. Nothing to fill it in with // because we don't serialize SSR stack traces for security reasons error.stack = ""; serialized[key] = error; } } else { serialized[key] = val; } } return serialized; } //#endregion //////////////////////////////////////////////////////////////////////////////// //#region Contexts //////////////////////////////////////////////////////////////////////////////// type ViewTransitionContextObject = | { isTransitioning: false; } | { isTransitioning: true; flushSync: boolean; currentLocation: Location; nextLocation: Location; }; const ViewTransitionContext = React.createContext({ isTransitioning: false, }); if (__DEV__) { ViewTransitionContext.displayName = "ViewTransition"; } export { ViewTransitionContext as UNSAFE_ViewTransitionContext }; // TODO: (v7) Change the useFetcher data from `any` to `unknown` type FetchersContextObject = Map; const FetchersContext = React.createContext(new Map()); if (__DEV__) { FetchersContext.displayName = "Fetchers"; } export { FetchersContext as UNSAFE_FetchersContext }; //#endregion //////////////////////////////////////////////////////////////////////////////// //#region Components //////////////////////////////////////////////////////////////////////////////// /** Webpack + React 17 fails to compile on any of the following because webpack complains that `startTransition` doesn't exist in `React`: * import { startTransition } from "react" * import * as React from from "react"; "startTransition" in React ? React.startTransition(() => setState()) : setState() * import * as React from from "react"; "startTransition" in React ? React["startTransition"](() => setState()) : setState() Moving it to a constant such as the following solves the Webpack/React 17 issue: * import * as React from from "react"; const START_TRANSITION = "startTransition"; START_TRANSITION in React ? React[START_TRANSITION](() => setState()) : setState() However, that introduces webpack/terser minification issues in production builds in React 18 where minification/obfuscation ends up removing the call of React.startTransition entirely from the first half of the ternary. Grabbing this exported reference once up front resolves that issue. See https://github.com/remix-run/react-router/issues/10579 */ const START_TRANSITION = "startTransition"; const startTransitionImpl = React[START_TRANSITION]; const FLUSH_SYNC = "flushSync"; const flushSyncImpl = ReactDOM[FLUSH_SYNC]; const USE_ID = "useId"; const useIdImpl = React[USE_ID]; function startTransitionSafe(cb: () => void) { if (startTransitionImpl) { startTransitionImpl(cb); } else { cb(); } } function flushSyncSafe(cb: () => void) { if (flushSyncImpl) { flushSyncImpl(cb); } else { cb(); } } interface ViewTransition { finished: Promise; ready: Promise; updateCallbackDone: Promise; skipTransition(): void; } class Deferred { status: "pending" | "resolved" | "rejected" = "pending"; promise: Promise; // @ts-expect-error - no initializer resolve: (value: T) => void; // @ts-expect-error - no initializer reject: (reason?: unknown) => void; constructor() { this.promise = new Promise((resolve, reject) => { this.resolve = (value) => { if (this.status === "pending") { this.status = "resolved"; resolve(value); } }; this.reject = (reason) => { if (this.status === "pending") { this.status = "rejected"; reject(reason); } }; }); } } /** * Given a Remix Router instance, render the appropriate UI */ export function RouterProvider({ fallbackElement, router, future, }: RouterProviderProps): React.ReactElement { let [state, setStateImpl] = React.useState(router.state); let [pendingState, setPendingState] = React.useState(); let [vtContext, setVtContext] = React.useState({ isTransitioning: false, }); let [renderDfd, setRenderDfd] = React.useState>(); let [transition, setTransition] = React.useState(); let [interruption, setInterruption] = React.useState<{ state: RouterState; currentLocation: Location; nextLocation: Location; }>(); let fetcherData = React.useRef>(new Map()); let { v7_startTransition } = future || {}; let optInStartTransition = React.useCallback( (cb: () => void) => { if (v7_startTransition) { startTransitionSafe(cb); } else { cb(); } }, [v7_startTransition] ); let setState = React.useCallback( ( newState: RouterState, { deletedFetchers, unstable_flushSync: flushSync, unstable_viewTransitionOpts: viewTransitionOpts, } ) => { deletedFetchers.forEach((key) => fetcherData.current.delete(key)); newState.fetchers.forEach((fetcher, key) => { if (fetcher.data !== undefined) { fetcherData.current.set(key, fetcher.data); } }); let isViewTransitionUnavailable = router.window == null || router.window.document == null || typeof router.window.document.startViewTransition !== "function"; // If this isn't a view transition or it's not available in this browser, // just update and be done with it if (!viewTransitionOpts || isViewTransitionUnavailable) { if (flushSync) { flushSyncSafe(() => setStateImpl(newState)); } else { optInStartTransition(() => setStateImpl(newState)); } return; } // flushSync + startViewTransition if (flushSync) { // Flush through the context to mark DOM elements as transition=ing flushSyncSafe(() => { // Cancel any pending transitions if (transition) { renderDfd && renderDfd.resolve(); transition.skipTransition(); } setVtContext({ isTransitioning: true, flushSync: true, currentLocation: viewTransitionOpts.currentLocation, nextLocation: viewTransitionOpts.nextLocation, }); }); // Update the DOM let t = router.window!.document.startViewTransition(() => { flushSyncSafe(() => setStateImpl(newState)); }); // Clean up after the animation completes t.finished.finally(() => { flushSyncSafe(() => { setRenderDfd(undefined); setTransition(undefined); setPendingState(undefined); setVtContext({ isTransitioning: false }); }); }); flushSyncSafe(() => setTransition(t)); return; } // startTransition + startViewTransition if (transition) { // Interrupting an in-progress transition, cancel and let everything flush // out, and then kick off a new transition from the interruption state renderDfd && renderDfd.resolve(); transition.skipTransition(); setInterruption({ state: newState, currentLocation: viewTransitionOpts.currentLocation, nextLocation: viewTransitionOpts.nextLocation, }); } else { // Completed navigation update with opted-in view transitions, let 'er rip setPendingState(newState); setVtContext({ isTransitioning: true, flushSync: false, currentLocation: viewTransitionOpts.currentLocation, nextLocation: viewTransitionOpts.nextLocation, }); } }, [router.window, transition, renderDfd, fetcherData, optInStartTransition] ); // Need to use a layout effect here so we are subscribed early enough to // pick up on any render-driven redirects/navigations (useEffect/) React.useLayoutEffect(() => router.subscribe(setState), [router, setState]); // When we start a view transition, create a Deferred we can use for the // eventual "completed" render React.useEffect(() => { if (vtContext.isTransitioning && !vtContext.flushSync) { setRenderDfd(new Deferred()); } }, [vtContext]); // Once the deferred is created, kick off startViewTransition() to update the // DOM and then wait on the Deferred to resolve (indicating the DOM update has // happened) React.useEffect(() => { if (renderDfd && pendingState && router.window) { let newState = pendingState; let renderPromise = renderDfd.promise; let transition = router.window.document.startViewTransition(async () => { optInStartTransition(() => setStateImpl(newState)); await renderPromise; }); transition.finished.finally(() => { setRenderDfd(undefined); setTransition(undefined); setPendingState(undefined); setVtContext({ isTransitioning: false }); }); setTransition(transition); } }, [optInStartTransition, pendingState, renderDfd, router.window]); // When the new location finally renders and is committed to the DOM, this // effect will run to resolve the transition React.useEffect(() => { if ( renderDfd && pendingState && state.location.key === pendingState.location.key ) { renderDfd.resolve(); } }, [renderDfd, transition, state.location, pendingState]); // If we get interrupted with a new navigation during a transition, we skip // the active transition, let it cleanup, then kick it off again here React.useEffect(() => { if (!vtContext.isTransitioning && interruption) { setPendingState(interruption.state); setVtContext({ isTransitioning: true, flushSync: false, currentLocation: interruption.currentLocation, nextLocation: interruption.nextLocation, }); setInterruption(undefined); } }, [vtContext.isTransitioning, interruption]); React.useEffect(() => { warning( fallbackElement == null || !router.future.v7_partialHydration, "`` is deprecated when using " + "`v7_partialHydration`, use a `HydrateFallback` component instead" ); // Only log this once on initial mount // eslint-disable-next-line react-hooks/exhaustive-deps }, []); let navigator = React.useMemo((): Navigator => { return { createHref: router.createHref, encodeLocation: router.encodeLocation, go: (n) => router.navigate(n), push: (to, state, opts) => router.navigate(to, { state, preventScrollReset: opts?.preventScrollReset, }), replace: (to, state, opts) => router.navigate(to, { replace: true, state, preventScrollReset: opts?.preventScrollReset, }), }; }, [router]); let basename = router.basename || "/"; let dataRouterContext = React.useMemo( () => ({ router, navigator, static: false, basename, }), [router, navigator, basename] ); // The fragment and {null} here are important! We need them to keep React 18's // useId happy when we are server-rendering since we may have a