| 1 | /**
|
| 2 | * react-router v8.3.1
|
| 3 | *
|
| 4 | * Copyright (c) Remix Software Inc.
|
| 5 | *
|
| 6 | * This source code is licensed under the MIT license found in the
|
| 7 | * LICENSE.md file in the root directory of this source tree.
|
| 8 | *
|
| 9 | * @license MIT
|
| 10 | */
|
| 11 | import { createPath, invariant, parsePath, warning } from "./router/history.js";
|
| 12 | import { convertRouteMatchToUiMatch, decodePath, getResolveToMatches, getRoutePattern, isBrowser, isRouteErrorResponse, joinPaths, matchPath, matchRoutes, parseToInfo, resolveTo, stripBasename } from "./router/utils.js";
|
| 13 | import { getNavigatorCurrentUrl, validateNavigationTarget } from "./router/navigation.js";
|
| 14 | import { IDLE_BLOCKER, hasInvalidProtocol } from "./router/router.js";
|
| 15 | import { AwaitContext, DataRouterContext, DataRouterStateContext, LocationContext, NavigationContext, RSCRouterContext, RouteContext, RouteErrorContext } from "./context.js";
|
| 16 | import { decodeRedirectErrorDigest, decodeRouteErrorResponseDigest } from "./errors.js";
|
| 17 | import * as React$1 from "react";
|
| 18 | //#region lib/hooks.tsx
|
| 19 | /**
|
| 20 | * Resolves a URL against the current {@link Location}.
|
| 21 | *
|
| 22 | * @example
|
| 23 | * import { useHref } from "react-router";
|
| 24 | *
|
| 25 | * function SomeComponent() {
|
| 26 | * let href = useHref("some/where");
|
| 27 | * // "/resolved/some/where"
|
| 28 | * }
|
| 29 | *
|
| 30 | * @public
|
| 31 | * @category Hooks
|
| 32 | * @param to The path to resolve
|
| 33 | * @param options Options
|
| 34 | * @param options.relative Defaults to `"route"` so routing is relative to the
|
| 35 | * route tree.
|
| 36 | * Set to `"path"` to make relative routing operate against path segments.
|
| 37 | * @returns The resolved href string
|
| 38 | */
|
| 39 | function useHref(to, { relative } = {}) {
|
| 40 | invariant(useInRouterContext(), `useHref() may be used only in the context of a <Router> component.`);
|
| 41 | let { basename, navigator } = React$1.useContext(NavigationContext);
|
| 42 | let { hash, pathname, search } = useResolvedPath(to, { relative });
|
| 43 | let joinedPathname = pathname;
|
| 44 | if (basename !== "/") joinedPathname = pathname === "/" ? basename : joinPaths([basename, pathname]);
|
| 45 | return navigator.createHref({
|
| 46 | pathname: joinedPathname,
|
| 47 | search,
|
| 48 | hash
|
| 49 | });
|
| 50 | }
|
| 51 | /**
|
| 52 | * Returns `true` if this component is a descendant of a {@link Router}, useful
|
| 53 | * to ensure a component is used within a {@link Router}.
|
| 54 | *
|
| 55 | * @public
|
| 56 | * @category Hooks
|
| 57 | * @mode framework
|
| 58 | * @mode data
|
| 59 | * @returns Whether the component is within a {@link Router} context
|
| 60 | */
|
| 61 | function useInRouterContext() {
|
| 62 | return React$1.useContext(LocationContext) != null;
|
| 63 | }
|
| 64 | /**
|
| 65 | * Returns the current {@link Location}. This can be useful if you'd like to
|
| 66 | * perform some side effect whenever it changes.
|
| 67 | *
|
| 68 | * @example
|
| 69 | * import * as React from 'react'
|
| 70 | * import { useLocation } from 'react-router'
|
| 71 | *
|
| 72 | * function SomeComponent() {
|
| 73 | * let location = useLocation()
|
| 74 | *
|
| 75 | * React.useEffect(() => {
|
| 76 | * // Google Analytics
|
| 77 | * ga('send', 'pageview')
|
| 78 | * }, [location]);
|
| 79 | *
|
| 80 | * return (
|
| 81 | * // ...
|
| 82 | * );
|
| 83 | * }
|
| 84 | *
|
| 85 | * @public
|
| 86 | * @category Hooks
|
| 87 | * @returns The current {@link Location} object
|
| 88 | */
|
| 89 | function useLocation() {
|
| 90 | invariant(useInRouterContext(), `useLocation() may be used only in the context of a <Router> component.`);
|
| 91 | return React$1.useContext(LocationContext).location;
|
| 92 | }
|
| 93 | /**
|
| 94 | * Returns the current {@link Navigation} action which describes how the router
|
| 95 | * came to the current {@link Location}, either by a pop, push, or replace on
|
| 96 | * the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) stack.
|
| 97 | *
|
| 98 | * @public
|
| 99 | * @category Hooks
|
| 100 | * @returns The current {@link NavigationType} (`"POP"`, `"PUSH"`, or `"REPLACE"`)
|
| 101 | */
|
| 102 | function useNavigationType() {
|
| 103 | return React$1.useContext(LocationContext).navigationType;
|
| 104 | }
|
| 105 | /**
|
| 106 | * Returns a {@link PathMatch} object if the given pattern matches the current URL.
|
| 107 | * This is useful for components that need to know "active" state, e.g.
|
| 108 | * {@link NavLink | `<NavLink>`}.
|
| 109 | *
|
| 110 | * @public
|
| 111 | * @category Hooks
|
| 112 | * @param pattern The pattern to match against the current {@link Location}
|
| 113 | * @returns The path match object if the pattern matches, `null` otherwise
|
| 114 | */
|
| 115 | function useMatch(pattern) {
|
| 116 | invariant(useInRouterContext(), `useMatch() may be used only in the context of a <Router> component.`);
|
| 117 | let { pathname } = useLocation();
|
| 118 | return React$1.useMemo(() => matchPath(pattern, decodePath(pathname)), [pathname, pattern]);
|
| 119 | }
|
| 120 | const navigateEffectWarning = "You should call navigate() in a React.useEffect(), not when your component is first rendered.";
|
| 121 | /**
|
| 122 | * Returns a function that lets you navigate programmatically in the browser in
|
| 123 | * response to user interactions or effects.
|
| 124 | *
|
| 125 | * It's often better to use {@link redirect} in [`action`](../../start/framework/route-module#action)/[`loader`](../../start/framework/route-module#loader)
|
| 126 | * functions than this hook.
|
| 127 | *
|
| 128 | * The returned function signature is `navigate(to, options?)`/`navigate(delta)` where:
|
| 129 | *
|
| 130 | * * `to` can be a string path, a {@link To} object, or a number (delta)
|
| 131 | * * `options` contains options for modifying the navigation
|
| 132 | * * These options work in all modes (Framework, Data, and Declarative):
|
| 133 | * * `relative`: `"route"` or `"path"` to control relative routing logic
|
| 134 | * * `replace`: Replace the current entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) stack
|
| 135 | * * `state`: Optional [`history.state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) to include with the new {@link Location}
|
| 136 | * * These options only work in Framework and Data modes:
|
| 137 | * * `flushSync`: Wrap the DOM updates in [`ReactDom.flushSync`](https://react.dev/reference/react-dom/flushSync)
|
| 138 | * * `preventScrollReset`: Do not scroll back to the top of the page after navigation
|
| 139 | * * `viewTransition`: Enable [`document.startViewTransition`](https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition) for this navigation
|
| 140 | *
|
| 141 | * @example
|
| 142 | * import { useNavigate } from "react-router";
|
| 143 | *
|
| 144 | * function SomeComponent() {
|
| 145 | * let navigate = useNavigate();
|
| 146 | * return (
|
| 147 | * <button onClick={() => navigate(-1)}>
|
| 148 | * Go Back
|
| 149 | * </button>
|
| 150 | * );
|
| 151 | * }
|
| 152 | *
|
| 153 | * @additionalExamples
|
| 154 | * ### Navigate to another path
|
| 155 | *
|
| 156 | * ```tsx
|
| 157 | * navigate("/some/route");
|
| 158 | * navigate("/some/route?search=param");
|
| 159 | * ```
|
| 160 | *
|
| 161 | * ### Navigate with a {@link To} object
|
| 162 | *
|
| 163 | * All properties are optional.
|
| 164 | *
|
| 165 | * ```tsx
|
| 166 | * navigate(
|
| 167 | * {
|
| 168 | * pathname: "/some/route",
|
| 169 | * search: "?search=param",
|
| 170 | * hash: "#hash",
|
| 171 | * },
|
| 172 | * {
|
| 173 | * state: { some: "state" },
|
| 174 | * },
|
| 175 | * );
|
| 176 | * ```
|
| 177 | *
|
| 178 | * If you use `state`, that will be available on the {@link Location} object on
|
| 179 | * the next page. Access it with `useLocation().state` (see {@link useLocation}).
|
| 180 | *
|
| 181 | * ### Navigate back or forward in the history stack
|
| 182 | *
|
| 183 | * ```tsx
|
| 184 | * // back
|
| 185 | * // often used to close modals
|
| 186 | * navigate(-1);
|
| 187 | *
|
| 188 | * // forward
|
| 189 | * // often used in a multistep wizard workflows
|
| 190 | * navigate(1);
|
| 191 | * ```
|
| 192 | *
|
| 193 | * Be cautious with `navigate(number)`. If your application can load up to a
|
| 194 | * route that has a button that tries to navigate forward/back, there may not be
|
| 195 | * a [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)
|
| 196 | * entry to go back or forward to, or it can go somewhere you don't expect
|
| 197 | * (like a different domain).
|
| 198 | *
|
| 199 | * Only use this if you're sure they will have an entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)
|
| 200 | * stack to navigate to.
|
| 201 | *
|
| 202 | * ### Replace the current entry in the history stack
|
| 203 | *
|
| 204 | * This will remove the current entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)
|
| 205 | * stack, replacing it with a new one, similar to a server side redirect.
|
| 206 | *
|
| 207 | * ```tsx
|
| 208 | * navigate("/some/route", { replace: true });
|
| 209 | * ```
|
| 210 | *
|
| 211 | * ### Prevent Scroll Reset
|
| 212 | *
|
| 213 | * [MODES: framework, data]
|
| 214 | *
|
| 215 | * <br/>
|
| 216 | * <br/>
|
| 217 | *
|
| 218 | * To prevent {@link ScrollRestoration | `<ScrollRestoration>`} from resetting
|
| 219 | * the scroll position, use the `preventScrollReset` option.
|
| 220 | *
|
| 221 | * ```tsx
|
| 222 | * navigate("?some-tab=1", { preventScrollReset: true });
|
| 223 | * ```
|
| 224 | *
|
| 225 | * For example, if you have a tab interface connected to search params in the
|
| 226 | * middle of a page, and you don't want it to scroll to the top when a tab is
|
| 227 | * clicked.
|
| 228 | *
|
| 229 | * ### Return Type Augmentation
|
| 230 | *
|
| 231 | * Internally, `useNavigate` uses a separate implementation when you are in
|
| 232 | * Declarative mode versus Data/Framework mode - the primary difference being
|
| 233 | * that the latter is able to return a stable reference that does not change
|
| 234 | * identity across navigations. The implementation in Data/Framework mode also
|
| 235 | * returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
|
| 236 | * that resolves when the navigation is completed. This means the return type of
|
| 237 | * `useNavigate` is `void | Promise<void>`. This is accurate, but can lead to
|
| 238 | * some red squigglies based on the union in the return value:
|
| 239 | *
|
| 240 | * - If you're using `typescript-eslint`, you may see errors from
|
| 241 | * [`@typescript-eslint/no-floating-promises`](https://typescript-eslint.io/rules/no-floating-promises)
|
| 242 | * - In Framework/Data mode, `React.use(navigate())` will show a false-positive
|
| 243 | * `Argument of type 'void | Promise<void>' is not assignable to parameter of
|
| 244 | * type 'Usable<void>'` error
|
| 245 | *
|
| 246 | * The easiest way to work around these issues is to augment the type based on the
|
| 247 | * router you're using:
|
| 248 | *
|
| 249 | * ```ts
|
| 250 | * // If using <BrowserRouter>
|
| 251 | * declare module "react-router" {
|
| 252 | * interface NavigateFunction {
|
| 253 | * (to: To, options?: NavigateOptions): void;
|
| 254 | * (delta: number): void;
|
| 255 | * }
|
| 256 | * }
|
| 257 | *
|
| 258 | * // If using <RouterProvider> or Framework mode
|
| 259 | * declare module "react-router" {
|
| 260 | * interface NavigateFunction {
|
| 261 | * (to: To, options?: NavigateOptions): Promise<void>;
|
| 262 | * (delta: number): Promise<void>;
|
| 263 | * }
|
| 264 | * }
|
| 265 | * ```
|
| 266 | *
|
| 267 | * @public
|
| 268 | * @category Hooks
|
| 269 | * @returns A navigate function for programmatic navigation
|
| 270 | */
|
| 271 | function useNavigate() {
|
| 272 | let { isDataRoute } = React$1.useContext(RouteContext);
|
| 273 | return isDataRoute ? useNavigateStable() : useNavigateUnstable();
|
| 274 | }
|
| 275 | function useNavigateUnstable() {
|
| 276 | invariant(useInRouterContext(), `useNavigate() may be used only in the context of a <Router> component.`);
|
| 277 | let dataRouterContext = React$1.useContext(DataRouterContext);
|
| 278 | let { basename, navigator } = React$1.useContext(NavigationContext);
|
| 279 | let { matches } = React$1.useContext(RouteContext);
|
| 280 | let { pathname: locationPathname } = useLocation();
|
| 281 | let routePathnamesJson = JSON.stringify(getResolveToMatches(matches));
|
| 282 | let activeRef = React$1.useRef(false);
|
| 283 | React$1.useLayoutEffect(() => {
|
| 284 | activeRef.current = true;
|
| 285 | });
|
| 286 | return React$1.useCallback((to, options = {}) => {
|
| 287 | warning(activeRef.current, navigateEffectWarning);
|
| 288 | if (!activeRef.current) return;
|
| 289 | if (typeof to === "number") {
|
| 290 | navigator.go(to);
|
| 291 | return;
|
| 292 | }
|
| 293 | let path = resolveTo(to, JSON.parse(routePathnamesJson), locationPathname, options.relative === "path");
|
| 294 | if (dataRouterContext == null && basename !== "/") path.pathname = path.pathname === "/" ? basename : joinPaths([basename, path.pathname]);
|
| 295 | validateNavigationTarget(typeof to === "string" ? to : createPath(to), navigator.createHref(path), getNavigatorCurrentUrl(navigator), "reject");
|
| 296 | (!!options.replace ? navigator.replace : navigator.push)(path, options.state, options);
|
| 297 | }, [
|
| 298 | basename,
|
| 299 | navigator,
|
| 300 | routePathnamesJson,
|
| 301 | locationPathname,
|
| 302 | dataRouterContext
|
| 303 | ]);
|
| 304 | }
|
| 305 | const OutletContext = React$1.createContext(null);
|
| 306 | /**
|
| 307 | * Returns the parent route {@link Outlet | `<Outlet context>`}.
|
| 308 | *
|
| 309 | * Often parent routes manage state or other values you want shared with child
|
| 310 | * routes. You can create your own [context provider](https://react.dev/learn/passing-data-deeply-with-context)
|
| 311 | * if you like, but this is such a common situation that it's built-into
|
| 312 | * {@link Outlet | `<Outlet>`}.
|
| 313 | *
|
| 314 | * ```tsx
|
| 315 | * // Parent route
|
| 316 | * function Parent() {
|
| 317 | * const [count, setCount] = React.useState(0);
|
| 318 | * return <Outlet context={[count, setCount]} />;
|
| 319 | * }
|
| 320 | * ```
|
| 321 | *
|
| 322 | * ```tsx
|
| 323 | * // Child route
|
| 324 | * import { useOutletContext } from "react-router";
|
| 325 | *
|
| 326 | * function Child() {
|
| 327 | * const [count, setCount] = useOutletContext();
|
| 328 | * const increment = () => setCount((c) => c + 1);
|
| 329 | * return <button onClick={increment}>{count}</button>;
|
| 330 | * }
|
| 331 | * ```
|
| 332 | *
|
| 333 | * If you're using TypeScript, we recommend the parent component provide a
|
| 334 | * custom hook for accessing the context value. This makes it easier for
|
| 335 | * consumers to get nice typings, control consumers, and know who's consuming
|
| 336 | * the context value.
|
| 337 | *
|
| 338 | * Here's a more realistic example:
|
| 339 | *
|
| 340 | * ```tsx filename=src/routes/dashboard.tsx lines=[14,20]
|
| 341 | * import { useState } from "react";
|
| 342 | * import { Outlet, useOutletContext } from "react-router";
|
| 343 | *
|
| 344 | * import type { User } from "./types";
|
| 345 | *
|
| 346 | * type ContextType = { user: User | null };
|
| 347 | *
|
| 348 | * export default function Dashboard() {
|
| 349 | * const [user, setUser] = useState<User | null>(null);
|
| 350 | *
|
| 351 | * return (
|
| 352 | * <div>
|
| 353 | * <h1>Dashboard</h1>
|
| 354 | * <Outlet context={{ user } satisfies ContextType} />
|
| 355 | * </div>
|
| 356 | * );
|
| 357 | * }
|
| 358 | *
|
| 359 | * export function useUser() {
|
| 360 | * return useOutletContext<ContextType>();
|
| 361 | * }
|
| 362 | * ```
|
| 363 | *
|
| 364 | * ```tsx filename=src/routes/dashboard/messages.tsx lines=[1,4]
|
| 365 | * import { useUser } from "../dashboard";
|
| 366 | *
|
| 367 | * export default function DashboardMessages() {
|
| 368 | * const { user } = useUser();
|
| 369 | * return (
|
| 370 | * <div>
|
| 371 | * <h2>Messages</h2>
|
| 372 | * <p>Hello, {user.name}!</p>
|
| 373 | * </div>
|
| 374 | * );
|
| 375 | * }
|
| 376 | * ```
|
| 377 | *
|
| 378 | * @public
|
| 379 | * @category Hooks
|
| 380 | * @returns The context value passed to the parent {@link Outlet} component
|
| 381 | */
|
| 382 | function useOutletContext() {
|
| 383 | return React$1.useContext(OutletContext);
|
| 384 | }
|
| 385 | /**
|
| 386 | * Returns the element for the child route at this level of the route
|
| 387 | * hierarchy. Used internally by {@link Outlet | `<Outlet>`} to render child
|
| 388 | * routes.
|
| 389 | *
|
| 390 | * @public
|
| 391 | * @category Hooks
|
| 392 | * @param context The context to pass to the outlet
|
| 393 | * @returns The child route element or `null` if no child routes match
|
| 394 | */
|
| 395 | function useOutlet(context) {
|
| 396 | let outlet = React$1.useContext(RouteContext).outlet;
|
| 397 | return React$1.useMemo(() => outlet && /* @__PURE__ */ React$1.createElement(OutletContext.Provider, { value: context }, outlet), [outlet, context]);
|
| 398 | }
|
| 399 | /**
|
| 400 | * Returns an object of key/value-pairs of the dynamic params from the current
|
| 401 | * URL that were matched by the routes. Child routes inherit all params from
|
| 402 | * their parent routes.
|
| 403 | *
|
| 404 | * Assuming a route pattern like `/posts/:postId` is matched by `/posts/123`
|
| 405 | * then `params.postId` will be `"123"`.
|
| 406 | *
|
| 407 | * @example
|
| 408 | * import { useParams } from "react-router";
|
| 409 | *
|
| 410 | * function SomeComponent() {
|
| 411 | * let params = useParams();
|
| 412 | * params.postId;
|
| 413 | * }
|
| 414 | *
|
| 415 | * @additionalExamples
|
| 416 | * ### Basic Usage
|
| 417 | *
|
| 418 | * ```tsx
|
| 419 | * import { useParams } from "react-router";
|
| 420 | *
|
| 421 | * // given a route like:
|
| 422 | * <Route path="/posts/:postId" element={<Post />} />;
|
| 423 | *
|
| 424 | * // or a data route like:
|
| 425 | * createBrowserRouter([
|
| 426 | * {
|
| 427 | * path: "/posts/:postId",
|
| 428 | * component: Post,
|
| 429 | * },
|
| 430 | * ]);
|
| 431 | *
|
| 432 | * // or in routes.ts
|
| 433 | * route("/posts/:postId", "routes/post.tsx");
|
| 434 | * ```
|
| 435 | *
|
| 436 | * Access the params in a component:
|
| 437 | *
|
| 438 | * ```tsx
|
| 439 | * import { useParams } from "react-router";
|
| 440 | *
|
| 441 | * export default function Post() {
|
| 442 | * let params = useParams();
|
| 443 | * return <h1>Post: {params.postId}</h1>;
|
| 444 | * }
|
| 445 | * ```
|
| 446 | *
|
| 447 | * ### Multiple Params
|
| 448 | *
|
| 449 | * Patterns can have multiple params:
|
| 450 | *
|
| 451 | * ```tsx
|
| 452 | * "/posts/:postId/comments/:commentId";
|
| 453 | * ```
|
| 454 | *
|
| 455 | * All will be available in the params object:
|
| 456 | *
|
| 457 | * ```tsx
|
| 458 | * import { useParams } from "react-router";
|
| 459 | *
|
| 460 | * export default function Post() {
|
| 461 | * let params = useParams();
|
| 462 | * return (
|
| 463 | * <h1>
|
| 464 | * Post: {params.postId}, Comment: {params.commentId}
|
| 465 | * </h1>
|
| 466 | * );
|
| 467 | * }
|
| 468 | * ```
|
| 469 | *
|
| 470 | * ### Catchall Params
|
| 471 | *
|
| 472 | * Catchall params are defined with `*`:
|
| 473 | *
|
| 474 | * ```tsx
|
| 475 | * "/files/*";
|
| 476 | * ```
|
| 477 | *
|
| 478 | * The matched value will be available in the params object as follows:
|
| 479 | *
|
| 480 | * ```tsx
|
| 481 | * import { useParams } from "react-router";
|
| 482 | *
|
| 483 | * export default function File() {
|
| 484 | * let params = useParams();
|
| 485 | * let catchall = params["*"];
|
| 486 | * // ...
|
| 487 | * }
|
| 488 | * ```
|
| 489 | *
|
| 490 | * You can destructure the catchall param:
|
| 491 | *
|
| 492 | * ```tsx
|
| 493 | * export default function File() {
|
| 494 | * let { "*": catchall } = useParams();
|
| 495 | * console.log(catchall);
|
| 496 | * }
|
| 497 | * ```
|
| 498 | *
|
| 499 | * @public
|
| 500 | * @category Hooks
|
| 501 | * @returns An object containing the dynamic route parameters
|
| 502 | */
|
| 503 | function useParams() {
|
| 504 | let { matches } = React$1.useContext(RouteContext);
|
| 505 | return matches[matches.length - 1]?.params ?? {};
|
| 506 | }
|
| 507 | /**
|
| 508 | * Resolves the pathname of the given `to` value against the current
|
| 509 | * {@link Location}. Similar to {@link useHref}, but returns a
|
| 510 | * {@link Path} instead of a string.
|
| 511 | *
|
| 512 | * @example
|
| 513 | * import { useResolvedPath } from "react-router";
|
| 514 | *
|
| 515 | * function SomeComponent() {
|
| 516 | * // if the user is at /dashboard/profile
|
| 517 | * let path = useResolvedPath("../accounts");
|
| 518 | * path.pathname; // "/dashboard/accounts"
|
| 519 | * path.search; // ""
|
| 520 | * path.hash; // ""
|
| 521 | * }
|
| 522 | *
|
| 523 | * @public
|
| 524 | * @category Hooks
|
| 525 | * @param to The path to resolve
|
| 526 | * @param options Options
|
| 527 | * @param options.relative Defaults to `"route"` so routing is relative to the route tree.
|
| 528 | * Set to `"path"` to make relative routing operate against path segments.
|
| 529 | * @returns The resolved {@link Path} object with `pathname`, `search`, and `hash`
|
| 530 | */
|
| 531 | function useResolvedPath(to, { relative } = {}) {
|
| 532 | let { matches } = React$1.useContext(RouteContext);
|
| 533 | let { pathname: locationPathname } = useLocation();
|
| 534 | let routePathnamesJson = JSON.stringify(getResolveToMatches(matches));
|
| 535 | return React$1.useMemo(() => resolveTo(to, JSON.parse(routePathnamesJson), locationPathname, relative === "path"), [
|
| 536 | to,
|
| 537 | routePathnamesJson,
|
| 538 | locationPathname,
|
| 539 | relative
|
| 540 | ]);
|
| 541 | }
|
| 542 | /**
|
| 543 | * Hook version of {@link Routes | `<Routes>`} that uses objects instead of
|
| 544 | * components. These objects have the same properties as the component props.
|
| 545 | * The return value of `useRoutes` is either a valid React element you can use
|
| 546 | * to render the route tree, or `null` if nothing matched.
|
| 547 | *
|
| 548 | * @example
|
| 549 | * import { useRoutes } from "react-router";
|
| 550 | *
|
| 551 | * function App() {
|
| 552 | * let element = useRoutes([
|
| 553 | * {
|
| 554 | * path: "/",
|
| 555 | * element: <Dashboard />,
|
| 556 | * children: [
|
| 557 | * {
|
| 558 | * path: "messages",
|
| 559 | * element: <DashboardMessages />,
|
| 560 | * },
|
| 561 | * { path: "tasks", element: <DashboardTasks /> },
|
| 562 | * ],
|
| 563 | * },
|
| 564 | * { path: "team", element: <AboutPage /> },
|
| 565 | * ]);
|
| 566 | *
|
| 567 | * return element;
|
| 568 | * }
|
| 569 | *
|
| 570 | * @public
|
| 571 | * @category Hooks
|
| 572 | * @param routes An array of {@link RouteObject}s that define the route hierarchy
|
| 573 | * @param locationArg An optional {@link Location} object or pathname string to
|
| 574 | * use instead of the current {@link Location}
|
| 575 | * @returns A React element to render the matched route, or `null` if no routes matched
|
| 576 | */
|
| 577 | function useRoutes(routes, locationArg) {
|
| 578 | return useRoutesImpl(routes, locationArg);
|
| 579 | }
|
| 580 | function useRoutesImpl(routes, locationArg, dataRouterOpts) {
|
| 581 | invariant(useInRouterContext(), `useRoutes() may be used only in the context of a <Router> component.`);
|
| 582 | let { navigator } = React$1.useContext(NavigationContext);
|
| 583 | let { matches: parentMatches } = React$1.useContext(RouteContext);
|
| 584 | let routeMatch = parentMatches[parentMatches.length - 1];
|
| 585 | let parentParams = routeMatch ? routeMatch.params : {};
|
| 586 | let parentPathname = routeMatch ? routeMatch.pathname : "/";
|
| 587 | let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";
|
| 588 | let parentRoute = routeMatch && routeMatch.route;
|
| 589 | {
|
| 590 | let parentPath = parentRoute && parentRoute.path || "";
|
| 591 | warningOnce(parentPathname, !parentRoute || parentPath.endsWith("*") || parentPath.endsWith("*?"), `You rendered descendant <Routes> (or called \`useRoutes()\`) at "${parentPathname}" (under <Route path="${parentPath}">) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render.\n\nPlease change the parent <Route path="${parentPath}"> to <Route path="${parentPath === "/" ? "*" : `${parentPath}/*`}">.`);
|
| 592 | }
|
| 593 | let locationFromContext = useLocation();
|
| 594 | let location;
|
| 595 | if (locationArg) {
|
| 596 | let parsedLocationArg = typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
|
| 597 | invariant(parentPathnameBase === "/" || parsedLocationArg.pathname?.startsWith(parentPathnameBase), `When overriding the location using \`<Routes location>\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${parentPathnameBase}" but pathname "${parsedLocationArg.pathname}" was given in the \`location\` prop.`);
|
| 598 | location = parsedLocationArg;
|
| 599 | } else location = locationFromContext;
|
| 600 | let pathname = location.pathname || "/";
|
| 601 | let remainingPathname = pathname;
|
| 602 | if (parentPathnameBase !== "/") {
|
| 603 | let parentSegments = parentPathnameBase.replace(/^\//, "").split("/");
|
| 604 | remainingPathname = "/" + pathname.replace(/^\//, "").split("/").slice(parentSegments.length).join("/");
|
| 605 | }
|
| 606 | let matches = dataRouterOpts && dataRouterOpts.state.matches.length ? dataRouterOpts.state.matches.map((m) => Object.assign(m, { route: dataRouterOpts.manifest[m.route.id] || m.route })) : matchRoutes(routes, { pathname: remainingPathname });
|
| 607 | warning(parentRoute || matches != null, `No routes matched location "${location.pathname}${location.search}${location.hash}" `);
|
| 608 | warning(matches == null || matches[matches.length - 1].route.element !== void 0 || matches[matches.length - 1].route.Component !== void 0 || matches[matches.length - 1].route.lazy !== void 0, `Matched leaf route at location "${location.pathname}${location.search}${location.hash}" does not have an element or Component. This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.`);
|
| 609 | let renderedMatches = _renderMatches(matches && matches.map((match) => Object.assign({}, match, {
|
| 610 | params: Object.assign({}, parentParams, match.params),
|
| 611 | pathname: joinPaths([parentPathnameBase, navigator.encodeLocation ? navigator.encodeLocation(match.pathname.replace(/%/g, "%25").replace(/\?/g, "%3F").replace(/#/g, "%23")).pathname : match.pathname]),
|
| 612 | pathnameBase: match.pathnameBase === "/" ? parentPathnameBase : joinPaths([parentPathnameBase, navigator.encodeLocation ? navigator.encodeLocation(match.pathnameBase.replace(/%/g, "%25").replace(/\?/g, "%3F").replace(/#/g, "%23")).pathname : match.pathnameBase])
|
| 613 | })), parentMatches, dataRouterOpts);
|
| 614 | if (locationArg && renderedMatches) return /* @__PURE__ */ React$1.createElement(LocationContext.Provider, { value: {
|
| 615 | location: {
|
| 616 | pathname: "/",
|
| 617 | search: "",
|
| 618 | hash: "",
|
| 619 | state: null,
|
| 620 | key: "default",
|
| 621 | mask: void 0,
|
| 622 | ...location
|
| 623 | },
|
| 624 | navigationType: "POP"
|
| 625 | } }, renderedMatches);
|
| 626 | return renderedMatches;
|
| 627 | }
|
| 628 | function DefaultErrorComponent() {
|
| 629 | let error = useRouteError();
|
| 630 | let message = isRouteErrorResponse(error) ? `${error.status} ${error.statusText}` : error instanceof Error ? error.message : JSON.stringify(error);
|
| 631 | let stack = error instanceof Error ? error.stack : null;
|
| 632 | let lightgrey = "rgba(200,200,200, 0.5)";
|
| 633 | let preStyles = {
|
| 634 | padding: "0.5rem",
|
| 635 | backgroundColor: lightgrey
|
| 636 | };
|
| 637 | let codeStyles = {
|
| 638 | padding: "2px 4px",
|
| 639 | backgroundColor: lightgrey
|
| 640 | };
|
| 641 | let devInfo = null;
|
| 642 | console.error("Error handled by React Router default ErrorBoundary:", error);
|
| 643 | devInfo = /* @__PURE__ */ React$1.createElement(React$1.Fragment, null, /* @__PURE__ */ React$1.createElement("p", null, "💿 Hey developer 👋"), /* @__PURE__ */ React$1.createElement("p", null, "You can provide a way better UX than this when your app throws errors by providing your own ", /* @__PURE__ */ React$1.createElement("code", { style: codeStyles }, "ErrorBoundary"), " or", " ", /* @__PURE__ */ React$1.createElement("code", { style: codeStyles }, "errorElement"), " prop on your route."));
|
| 644 | return /* @__PURE__ */ React$1.createElement(React$1.Fragment, null, /* @__PURE__ */ React$1.createElement("h2", null, "Unexpected Application Error!"), /* @__PURE__ */ React$1.createElement("h3", { style: { fontStyle: "italic" } }, message), stack ? /* @__PURE__ */ React$1.createElement("pre", { style: preStyles }, stack) : null, devInfo);
|
| 645 | }
|
| 646 | const defaultErrorElement = /* @__PURE__ */ React$1.createElement(DefaultErrorComponent, null);
|
| 647 | var RenderErrorBoundary = class extends React$1.Component {
|
| 648 | constructor(props) {
|
| 649 | super(props);
|
| 650 | this.state = {
|
| 651 | location: props.location,
|
| 652 | revalidation: props.revalidation,
|
| 653 | error: props.error
|
| 654 | };
|
| 655 | }
|
| 656 | static contextType = RSCRouterContext;
|
| 657 | static getDerivedStateFromError(error) {
|
| 658 | return { error };
|
| 659 | }
|
| 660 | static getDerivedStateFromProps(props, state) {
|
| 661 | if (state.location !== props.location || state.revalidation !== "idle" && props.revalidation === "idle") return {
|
| 662 | error: props.error,
|
| 663 | location: props.location,
|
| 664 | revalidation: props.revalidation
|
| 665 | };
|
| 666 | return {
|
| 667 | error: props.error !== void 0 ? props.error : state.error,
|
| 668 | location: state.location,
|
| 669 | revalidation: props.revalidation || state.revalidation
|
| 670 | };
|
| 671 | }
|
| 672 | componentDidCatch(error, errorInfo) {
|
| 673 | if (this.props.onError) this.props.onError(error, errorInfo);
|
| 674 | else console.error("React Router caught the following error during render", error);
|
| 675 | }
|
| 676 | render() {
|
| 677 | let error = this.state.error;
|
| 678 | if (this.context && typeof error === "object" && error && "digest" in error && typeof error.digest === "string") {
|
| 679 | const decoded = decodeRouteErrorResponseDigest(error.digest);
|
| 680 | if (decoded) error = decoded;
|
| 681 | }
|
| 682 | let result = error !== void 0 ? /* @__PURE__ */ React$1.createElement(RouteContext.Provider, { value: this.props.routeContext }, /* @__PURE__ */ React$1.createElement(RouteErrorContext.Provider, {
|
| 683 | value: error,
|
| 684 | children: this.props.component
|
| 685 | })) : this.props.children;
|
| 686 | if (this.context) return /* @__PURE__ */ React$1.createElement(RSCErrorHandler, { error }, result);
|
| 687 | return result;
|
| 688 | }
|
| 689 | };
|
| 690 | const errorRedirectHandledMap = /* @__PURE__ */ new WeakMap();
|
| 691 | function RSCErrorHandler({ children, error }) {
|
| 692 | let { basename, navigator } = React$1.useContext(NavigationContext);
|
| 693 | if (typeof error === "object" && error && "digest" in error && typeof error.digest === "string") {
|
| 694 | let redirect = decodeRedirectErrorDigest(error.digest);
|
| 695 | if (redirect) {
|
| 696 | let existingRedirect = errorRedirectHandledMap.get(error);
|
| 697 | if (existingRedirect) throw existingRedirect;
|
| 698 | let parsed = parseToInfo(redirect.location, basename);
|
| 699 | let target = parsed.absoluteURL || parsed.to;
|
| 700 | validateNavigationTarget(redirect.location, target, getNavigatorCurrentUrl(navigator), "allow-explicit");
|
| 701 | if (hasInvalidProtocol(target)) throw new Error("Invalid redirect location");
|
| 702 | if (isBrowser && !errorRedirectHandledMap.get(error)) if (parsed.isExternal || redirect.reloadDocument) window.location.href = target;
|
| 703 | else {
|
| 704 | const redirectPromise = Promise.resolve().then(() => window.__reactRouterDataRouter.navigate(parsed.to, { replace: redirect.replace }));
|
| 705 | errorRedirectHandledMap.set(error, redirectPromise);
|
| 706 | throw redirectPromise;
|
| 707 | }
|
| 708 | return /* @__PURE__ */ React$1.createElement("meta", {
|
| 709 | httpEquiv: "refresh",
|
| 710 | content: `0;url=${target}`
|
| 711 | });
|
| 712 | }
|
| 713 | }
|
| 714 | return children;
|
| 715 | }
|
| 716 | function RenderedRoute({ routeContext, match, children }) {
|
| 717 | let dataRouterContext = React$1.useContext(DataRouterContext);
|
| 718 | if (dataRouterContext && dataRouterContext.static && dataRouterContext.staticContext && (match.route.errorElement || match.route.ErrorBoundary)) dataRouterContext.staticContext._deepestRenderedBoundaryId = match.route.id;
|
| 719 | return /* @__PURE__ */ React$1.createElement(RouteContext.Provider, { value: routeContext }, children);
|
| 720 | }
|
| 721 | function _renderMatches(matches, parentMatches = [], dataRouterOpts) {
|
| 722 | let dataRouterState = dataRouterOpts?.state;
|
| 723 | if (matches == null) {
|
| 724 | if (!dataRouterState) return null;
|
| 725 | if (dataRouterState.errors) matches = dataRouterState.matches;
|
| 726 | else if (parentMatches.length === 0 && !dataRouterState.initialized && dataRouterState.matches.length > 0) matches = dataRouterState.matches;
|
| 727 | else return null;
|
| 728 | }
|
| 729 | let renderedMatches = matches;
|
| 730 | let errors = dataRouterState?.errors;
|
| 731 | if (errors != null) {
|
| 732 | let errorIndex = renderedMatches.findIndex((m) => m.route.id && errors?.[m.route.id] !== void 0);
|
| 733 | invariant(errorIndex >= 0, `Could not find a matching route for errors on route IDs: ${Object.keys(errors).join(",")}`);
|
| 734 | renderedMatches = renderedMatches.slice(0, Math.min(renderedMatches.length, errorIndex + 1));
|
| 735 | }
|
| 736 | let renderFallback = false;
|
| 737 | let fallbackIndex = -1;
|
| 738 | if (dataRouterOpts && dataRouterState) {
|
| 739 | renderFallback = dataRouterState.renderFallback;
|
| 740 | for (let i = 0; i < renderedMatches.length; i++) {
|
| 741 | let match = renderedMatches[i];
|
| 742 | if (match.route.HydrateFallback || match.route.hydrateFallbackElement) fallbackIndex = i;
|
| 743 | if (match.route.id) {
|
| 744 | let { loaderData, errors } = dataRouterState;
|
| 745 | let needsToRunLoader = match.route.loader && !loaderData.hasOwnProperty(match.route.id) && (!errors || errors[match.route.id] === void 0);
|
| 746 | if (match.route.lazy || needsToRunLoader) {
|
| 747 | if (dataRouterOpts.isStatic) renderFallback = true;
|
| 748 | if (fallbackIndex >= 0) renderedMatches = renderedMatches.slice(0, fallbackIndex + 1);
|
| 749 | else renderedMatches = [renderedMatches[0]];
|
| 750 | break;
|
| 751 | }
|
| 752 | }
|
| 753 | }
|
| 754 | }
|
| 755 | let onErrorHandler = dataRouterOpts?.onError;
|
| 756 | let onError = dataRouterState && onErrorHandler ? (error, errorInfo) => {
|
| 757 | onErrorHandler(error, {
|
| 758 | location: dataRouterState.location,
|
| 759 | params: dataRouterState.matches?.[0]?.params ?? {},
|
| 760 | pattern: getRoutePattern(dataRouterState.matches),
|
| 761 | errorInfo
|
| 762 | });
|
| 763 | } : void 0;
|
| 764 | return renderedMatches.reduceRight((outlet, match, index) => {
|
| 765 | let error;
|
| 766 | let shouldRenderHydrateFallback = false;
|
| 767 | let errorElement = null;
|
| 768 | let hydrateFallbackElement = null;
|
| 769 | if (dataRouterState) {
|
| 770 | error = errors && match.route.id ? errors[match.route.id] : void 0;
|
| 771 | errorElement = match.route.errorElement || defaultErrorElement;
|
| 772 | if (renderFallback) {
|
| 773 | if (fallbackIndex < 0 && index === 0) {
|
| 774 | warningOnce("route-fallback", false, "No `HydrateFallback` element provided to render during initial hydration");
|
| 775 | shouldRenderHydrateFallback = true;
|
| 776 | hydrateFallbackElement = null;
|
| 777 | } else if (fallbackIndex === index) {
|
| 778 | shouldRenderHydrateFallback = true;
|
| 779 | hydrateFallbackElement = match.route.hydrateFallbackElement || null;
|
| 780 | }
|
| 781 | }
|
| 782 | }
|
| 783 | let matches = parentMatches.concat(renderedMatches.slice(0, index + 1));
|
| 784 | let getChildren = () => {
|
| 785 | let children;
|
| 786 | if (error) children = errorElement;
|
| 787 | else if (shouldRenderHydrateFallback) children = hydrateFallbackElement;
|
| 788 | else if (match.route.Component) children = /* @__PURE__ */ React$1.createElement(match.route.Component, null);
|
| 789 | else if (match.route.element) children = match.route.element;
|
| 790 | else children = outlet;
|
| 791 | return /* @__PURE__ */ React$1.createElement(RenderedRoute, {
|
| 792 | match,
|
| 793 | routeContext: {
|
| 794 | outlet,
|
| 795 | matches,
|
| 796 | isDataRoute: dataRouterState != null
|
| 797 | },
|
| 798 | children
|
| 799 | });
|
| 800 | };
|
| 801 | return dataRouterState && (match.route.ErrorBoundary || match.route.errorElement || index === 0) ? /* @__PURE__ */ React$1.createElement(RenderErrorBoundary, {
|
| 802 | location: dataRouterState.location,
|
| 803 | revalidation: dataRouterState.revalidation,
|
| 804 | component: errorElement,
|
| 805 | error,
|
| 806 | children: getChildren(),
|
| 807 | routeContext: {
|
| 808 | outlet: null,
|
| 809 | matches,
|
| 810 | isDataRoute: true
|
| 811 | },
|
| 812 | onError
|
| 813 | }) : getChildren();
|
| 814 | }, null);
|
| 815 | }
|
| 816 | function getDataRouterConsoleError(hookName) {
|
| 817 | return `${hookName} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`;
|
| 818 | }
|
| 819 | function useDataRouterContext(hookName) {
|
| 820 | let ctx = React$1.useContext(DataRouterContext);
|
| 821 | invariant(ctx, getDataRouterConsoleError(hookName));
|
| 822 | return ctx;
|
| 823 | }
|
| 824 | function useDataRouterState(hookName) {
|
| 825 | let state = React$1.useContext(DataRouterStateContext);
|
| 826 | invariant(state, getDataRouterConsoleError(hookName));
|
| 827 | return state;
|
| 828 | }
|
| 829 | function useRouteContext(hookName) {
|
| 830 | let route = React$1.useContext(RouteContext);
|
| 831 | invariant(route, getDataRouterConsoleError(hookName));
|
| 832 | return route;
|
| 833 | }
|
| 834 | function useCurrentRouteId(hookName) {
|
| 835 | let route = useRouteContext(hookName);
|
| 836 | let thisRoute = route.matches[route.matches.length - 1];
|
| 837 | invariant(thisRoute.route.id, `${hookName} can only be used on routes that contain a unique "id"`);
|
| 838 | return thisRoute.route.id;
|
| 839 | }
|
| 840 | /**
|
| 841 | * Returns the ID for the nearest contextual route
|
| 842 | *
|
| 843 | * @category Hooks
|
| 844 | * @returns The ID of the nearest contextual route
|
| 845 | */
|
| 846 | function useRouteId() {
|
| 847 | return useCurrentRouteId("useRouteId");
|
| 848 | }
|
| 849 | /**
|
| 850 | * Returns the current {@link Navigation}, defaulting to an "idle" navigation
|
| 851 | * when no navigation is in progress. You can use this to render pending UI
|
| 852 | * (like a global spinner) or read [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
|
| 853 | * from a form navigation.
|
| 854 | *
|
| 855 | * @example
|
| 856 | * import { useNavigation } from "react-router";
|
| 857 | *
|
| 858 | * function SomeComponent() {
|
| 859 | * let navigation = useNavigation();
|
| 860 | * navigation.state;
|
| 861 | * navigation.formData;
|
| 862 | * // etc.
|
| 863 | * }
|
| 864 | *
|
| 865 | * @public
|
| 866 | * @category Hooks
|
| 867 | * @mode framework
|
| 868 | * @mode data
|
| 869 | * @returns The current {@link Navigation} object
|
| 870 | */
|
| 871 | function useNavigation() {
|
| 872 | let state = useDataRouterState("useNavigation");
|
| 873 | return React$1.useMemo(() => {
|
| 874 | let { matches, historyAction, ...rest } = state.navigation;
|
| 875 | return rest;
|
| 876 | }, [state.navigation]);
|
| 877 | }
|
| 878 | /**
|
| 879 | * Revalidate the data on the page for reasons outside of normal data mutations
|
| 880 | * like [`Window` focus](https://developer.mozilla.org/en-US/docs/Web/API/Window/focus_event)
|
| 881 | * or polling on an interval.
|
| 882 | *
|
| 883 | * Note that page data is already revalidated automatically after actions.
|
| 884 | * If you find yourself using this for normal CRUD operations on your data in
|
| 885 | * response to user interactions, you're probably not taking advantage of the
|
| 886 | * other APIs like {@link useFetcher}, {@link Form}, {@link useSubmit} that do
|
| 887 | * this automatically.
|
| 888 | *
|
| 889 | * @example
|
| 890 | * import { useRevalidator } from "react-router";
|
| 891 | *
|
| 892 | * function WindowFocusRevalidator() {
|
| 893 | * const revalidator = useRevalidator();
|
| 894 | *
|
| 895 | * useFakeWindowFocus(() => {
|
| 896 | * revalidator.revalidate();
|
| 897 | * });
|
| 898 | *
|
| 899 | * return (
|
| 900 | * <div hidden={revalidator.state === "idle"}>
|
| 901 | * Revalidating...
|
| 902 | * </div>
|
| 903 | * );
|
| 904 | * }
|
| 905 | *
|
| 906 | * @public
|
| 907 | * @category Hooks
|
| 908 | * @mode framework
|
| 909 | * @mode data
|
| 910 | * @returns An object with a `revalidate` function and the current revalidation
|
| 911 | * `state`
|
| 912 | */
|
| 913 | function useRevalidator() {
|
| 914 | let dataRouterContext = useDataRouterContext("useRevalidator");
|
| 915 | let state = useDataRouterState("useRevalidator");
|
| 916 | let revalidate = React$1.useCallback(async () => {
|
| 917 | await dataRouterContext.router.revalidate();
|
| 918 | }, [dataRouterContext.router]);
|
| 919 | return React$1.useMemo(() => ({
|
| 920 | revalidate,
|
| 921 | state: state.revalidation
|
| 922 | }), [revalidate, state.revalidation]);
|
| 923 | }
|
| 924 | /**
|
| 925 | * Returns the active route matches, useful for accessing `loaderData` for
|
| 926 | * parent/child routes or the route [`handle`](../../start/framework/route-module#handle)
|
| 927 | * property
|
| 928 | *
|
| 929 | * Pairing the route `handle` with `useMatches` gets very powerful since you can put
|
| 930 | * whatever you want on a route handle and have access to `useMatches` anywhere.
|
| 931 | * Please see the [handle](../../how-to/using-handle) documentation for an example
|
| 932 | * of breadcrumbs via `useMatches`/`handle`.
|
| 933 | *
|
| 934 | * ```tsx
|
| 935 | * import { useMatches } from "react-router";
|
| 936 | *
|
| 937 | * function SomeComponent() {
|
| 938 | * const matches = useMatches();
|
| 939 | * // matches[i].id // route id
|
| 940 | * // matches[i].pathname // the portion of the URL the route matched
|
| 941 | * // matches[i].params // the parsed params from the URL
|
| 942 | * // matches[i].loaderData // the data from the loader
|
| 943 | * // matches[i].handle // the route handle with any app specific data
|
| 944 | * }
|
| 945 | * ```
|
| 946 | *
|
| 947 | * <docs-info>useMatches only works with a data router like `createBrowserRouter`,
|
| 948 | * since they know the full route tree up front and can provide all of the current
|
| 949 | * matches. Additionally, `useMatches` will not match down into any descendant route
|
| 950 | * trees since the router isn't aware of the descendant routes.</docs-info>
|
| 951 | *
|
| 952 | * @public
|
| 953 | * @category Hooks
|
| 954 | * @mode framework
|
| 955 | * @mode data
|
| 956 | * @returns An array of {@link UIMatch | UI matches} for the current route hierarchy
|
| 957 | */
|
| 958 | function useMatches() {
|
| 959 | let { matches, loaderData } = useDataRouterState("useMatches");
|
| 960 | return React$1.useMemo(() => matches.map((m) => convertRouteMatchToUiMatch(m, loaderData)), [matches, loaderData]);
|
| 961 | }
|
| 962 | /**
|
| 963 | * Returns the data from the closest route
|
| 964 | * [`loader`](../../start/framework/route-module#loader) or
|
| 965 | * [`clientLoader`](../../start/framework/route-module#clientloader).
|
| 966 | *
|
| 967 | * @example
|
| 968 | * import { useLoaderData } from "react-router";
|
| 969 | *
|
| 970 | * export async function loader() {
|
| 971 | * return await fakeDb.invoices.findAll();
|
| 972 | * }
|
| 973 | *
|
| 974 | * export default function Invoices() {
|
| 975 | * let invoices = useLoaderData<typeof loader>();
|
| 976 | * // ...
|
| 977 | * }
|
| 978 | *
|
| 979 | * @public
|
| 980 | * @category Hooks
|
| 981 | * @mode framework
|
| 982 | * @mode data
|
| 983 | * @returns The data returned from the route's [`loader`](../../start/framework/route-module#loader) or [`clientLoader`](../../start/framework/route-module#clientloader) function
|
| 984 | */
|
| 985 | function useLoaderData() {
|
| 986 | let state = useDataRouterState("useLoaderData");
|
| 987 | let routeId = useCurrentRouteId("useLoaderData");
|
| 988 | return state.loaderData[routeId];
|
| 989 | }
|
| 990 | /**
|
| 991 | * Returns the [`loader`](../../start/framework/route-module#loader) data for a
|
| 992 | * given route by route ID.
|
| 993 | *
|
| 994 | * Route IDs are created automatically. They are simply the path of the route file
|
| 995 | * relative to the app folder without the extension.
|
| 996 | *
|
| 997 | * | Route Filename | Route ID |
|
| 998 | * | ---------------------------- | ---------------------- |
|
| 999 | * | `app/root.tsx` | `"root"` |
|
| 1000 | * | `app/routes/teams.tsx` | `"routes/teams"` |
|
| 1001 | * | `app/whatever/teams.$id.tsx` | `"whatever/teams.$id"` |
|
| 1002 | *
|
| 1003 | * @example
|
| 1004 | * import { useRouteLoaderData } from "react-router";
|
| 1005 | *
|
| 1006 | * function SomeComponent() {
|
| 1007 | * const { user } = useRouteLoaderData("root");
|
| 1008 | * }
|
| 1009 | *
|
| 1010 | * // You can also specify your own route ID's manually in your routes.ts file:
|
| 1011 | * route("/", "containers/app.tsx", { id: "app" })
|
| 1012 | * useRouteLoaderData("app");
|
| 1013 | *
|
| 1014 | * @public
|
| 1015 | * @category Hooks
|
| 1016 | * @mode framework
|
| 1017 | * @mode data
|
| 1018 | * @param routeId The ID of the route to return loader data from
|
| 1019 | * @returns The data returned from the specified route's [`loader`](../../start/framework/route-module#loader)
|
| 1020 | * function, or `undefined` if not found
|
| 1021 | */
|
| 1022 | function useRouteLoaderData(routeId) {
|
| 1023 | return useDataRouterState("useRouteLoaderData").loaderData[routeId];
|
| 1024 | }
|
| 1025 | /**
|
| 1026 | * Returns the [`action`](../../start/framework/route-module#action) data from
|
| 1027 | * the most recent `POST` navigation form submission or `undefined` if there
|
| 1028 | * hasn't been one.
|
| 1029 | *
|
| 1030 | * @example
|
| 1031 | * import { Form, useActionData } from "react-router";
|
| 1032 | *
|
| 1033 | * export async function action({ request }) {
|
| 1034 | * const body = await request.formData();
|
| 1035 | * const name = body.get("visitorsName");
|
| 1036 | * return { message: `Hello, ${name}` };
|
| 1037 | * }
|
| 1038 | *
|
| 1039 | * export default function Invoices() {
|
| 1040 | * const data = useActionData();
|
| 1041 | * return (
|
| 1042 | * <Form method="post">
|
| 1043 | * <input type="text" name="visitorsName" />
|
| 1044 | * {data ? data.message : "Waiting..."}
|
| 1045 | * </Form>
|
| 1046 | * );
|
| 1047 | * }
|
| 1048 | *
|
| 1049 | * @public
|
| 1050 | * @category Hooks
|
| 1051 | * @mode framework
|
| 1052 | * @mode data
|
| 1053 | * @returns The data returned from the route's [`action`](../../start/framework/route-module#action)
|
| 1054 | * function, or `undefined` if no [`action`](../../start/framework/route-module#action)
|
| 1055 | * has been called
|
| 1056 | */
|
| 1057 | function useActionData() {
|
| 1058 | let state = useDataRouterState("useActionData");
|
| 1059 | let routeId = useCurrentRouteId("useLoaderData");
|
| 1060 | return state.actionData ? state.actionData[routeId] : void 0;
|
| 1061 | }
|
| 1062 | /**
|
| 1063 | * Accesses the error thrown during an
|
| 1064 | * [`action`](../../start/framework/route-module#action),
|
| 1065 | * [`loader`](../../start/framework/route-module#loader),
|
| 1066 | * or component render to be used in a route module
|
| 1067 | * [`ErrorBoundary`](../../start/framework/route-module#errorboundary).
|
| 1068 | *
|
| 1069 | * @example
|
| 1070 | * export function ErrorBoundary() {
|
| 1071 | * const error = useRouteError();
|
| 1072 | * return <div>{error.message}</div>;
|
| 1073 | * }
|
| 1074 | *
|
| 1075 | * @public
|
| 1076 | * @category Hooks
|
| 1077 | * @mode framework
|
| 1078 | * @mode data
|
| 1079 | * @returns The error that was thrown during route [loading](../../start/framework/route-module#loader),
|
| 1080 | * [`action`](../../start/framework/route-module#action) execution, or rendering
|
| 1081 | */
|
| 1082 | function useRouteError() {
|
| 1083 | let error = React$1.useContext(RouteErrorContext);
|
| 1084 | let state = useDataRouterState("useRouteError");
|
| 1085 | let routeId = useCurrentRouteId("useRouteError");
|
| 1086 | if (error !== void 0) return error;
|
| 1087 | return state.errors?.[routeId];
|
| 1088 | }
|
| 1089 | /**
|
| 1090 | * Returns the resolved promise value from the closest {@link Await | `<Await>`}.
|
| 1091 | *
|
| 1092 | * @example
|
| 1093 | * function SomeDescendant() {
|
| 1094 | * const value = useAsyncValue();
|
| 1095 | * // ...
|
| 1096 | * }
|
| 1097 | *
|
| 1098 | * // somewhere in your app
|
| 1099 | * <Await resolve={somePromise}>
|
| 1100 | * <SomeDescendant />
|
| 1101 | * </Await>;
|
| 1102 | *
|
| 1103 | * @public
|
| 1104 | * @category Hooks
|
| 1105 | * @mode framework
|
| 1106 | * @mode data
|
| 1107 | * @returns The resolved value from the nearest {@link Await} component
|
| 1108 | */
|
| 1109 | function useAsyncValue() {
|
| 1110 | return React$1.useContext(AwaitContext)?._data;
|
| 1111 | }
|
| 1112 | /**
|
| 1113 | * Returns the rejection value from the closest {@link Await | `<Await>`}.
|
| 1114 | *
|
| 1115 | * @example
|
| 1116 | * import { Await, useAsyncError } from "react-router";
|
| 1117 | *
|
| 1118 | * function ErrorElement() {
|
| 1119 | * const error = useAsyncError();
|
| 1120 | * return (
|
| 1121 | * <p>Uh Oh, something went wrong! {error.message}</p>
|
| 1122 | * );
|
| 1123 | * }
|
| 1124 | *
|
| 1125 | * // somewhere in your app
|
| 1126 | * <Await
|
| 1127 | * resolve={promiseThatRejects}
|
| 1128 | * errorElement={<ErrorElement />}
|
| 1129 | * />;
|
| 1130 | *
|
| 1131 | * @public
|
| 1132 | * @category Hooks
|
| 1133 | * @mode framework
|
| 1134 | * @mode data
|
| 1135 | * @returns The error that was thrown in the nearest {@link Await} component
|
| 1136 | */
|
| 1137 | function useAsyncError() {
|
| 1138 | return React$1.useContext(AwaitContext)?._error;
|
| 1139 | }
|
| 1140 | let blockerId = 0;
|
| 1141 | /**
|
| 1142 | * Allow the application to block navigations within the SPA and present the
|
| 1143 | * user a confirmation dialog to confirm the navigation. Mostly used to avoid
|
| 1144 | * using half-filled form data. This does not handle hard-reloads or
|
| 1145 | * cross-origin navigations.
|
| 1146 | *
|
| 1147 | * The {@link Blocker} object returned by the hook has the following properties:
|
| 1148 | *
|
| 1149 | * - **`state`**
|
| 1150 | * - `unblocked` - the blocker is idle and has not prevented any navigation
|
| 1151 | * - `blocked` - the blocker has prevented a navigation
|
| 1152 | * - `proceeding` - the blocker is proceeding through from a blocked navigation
|
| 1153 | * - **`location`**
|
| 1154 | * - When in a `blocked` state, this represents the {@link Location} to which
|
| 1155 | * we blocked a navigation. When in a `proceeding` state, this is the
|
| 1156 | * location being navigated to after a `blocker.proceed()` call.
|
| 1157 | * - **`proceed()`**
|
| 1158 | * - When in a `blocked` state, you may call `blocker.proceed()` to proceed to
|
| 1159 | * the blocked location.
|
| 1160 | * - **`reset()`**
|
| 1161 | * - When in a `blocked` state, you may call `blocker.reset()` to return the
|
| 1162 | * blocker to an `unblocked` state and leave the user at the current
|
| 1163 | * location.
|
| 1164 | *
|
| 1165 | * @example
|
| 1166 | * // Boolean version
|
| 1167 | * let blocker = useBlocker(value !== "");
|
| 1168 | *
|
| 1169 | * // Function version
|
| 1170 | * let blocker = useBlocker(
|
| 1171 | * ({ currentLocation, nextLocation, historyAction }) =>
|
| 1172 | * value !== "" &&
|
| 1173 | * currentLocation.pathname !== nextLocation.pathname
|
| 1174 | * );
|
| 1175 | *
|
| 1176 | * @additionalExamples
|
| 1177 | * ```tsx
|
| 1178 | * import { useCallback, useState } from "react";
|
| 1179 | * import { BlockerFunction, useBlocker } from "react-router";
|
| 1180 | *
|
| 1181 | * export function ImportantForm() {
|
| 1182 | * const [value, setValue] = useState("");
|
| 1183 | *
|
| 1184 | * const shouldBlock = useCallback<BlockerFunction>(
|
| 1185 | * () => value !== "",
|
| 1186 | * [value]
|
| 1187 | * );
|
| 1188 | * const blocker = useBlocker(shouldBlock);
|
| 1189 | *
|
| 1190 | * return (
|
| 1191 | * <form
|
| 1192 | * onSubmit={(e) => {
|
| 1193 | * e.preventDefault();
|
| 1194 | * setValue("");
|
| 1195 | * if (blocker.state === "blocked") {
|
| 1196 | * blocker.proceed();
|
| 1197 | * }
|
| 1198 | * }}
|
| 1199 | * >
|
| 1200 | * <input
|
| 1201 | * name="data"
|
| 1202 | * value={value}
|
| 1203 | * onChange={(e) => setValue(e.target.value)}
|
| 1204 | * />
|
| 1205 | *
|
| 1206 | * <button type="submit">Save</button>
|
| 1207 | *
|
| 1208 | * {blocker.state === "blocked" ? (
|
| 1209 | * <>
|
| 1210 | * <p style={{ color: "red" }}>
|
| 1211 | * Blocked the last navigation to
|
| 1212 | * </p>
|
| 1213 | * <button
|
| 1214 | * type="button"
|
| 1215 | * onClick={() => blocker.proceed()}
|
| 1216 | * >
|
| 1217 | * Let me through
|
| 1218 | * </button>
|
| 1219 | * <button
|
| 1220 | * type="button"
|
| 1221 | * onClick={() => blocker.reset()}
|
| 1222 | * >
|
| 1223 | * Keep me here
|
| 1224 | * </button>
|
| 1225 | * </>
|
| 1226 | * ) : blocker.state === "proceeding" ? (
|
| 1227 | * <p style={{ color: "orange" }}>
|
| 1228 | * Proceeding through blocked navigation
|
| 1229 | * </p>
|
| 1230 | * ) : (
|
| 1231 | * <p style={{ color: "green" }}>
|
| 1232 | * Blocker is currently unblocked
|
| 1233 | * </p>
|
| 1234 | * )}
|
| 1235 | * </form>
|
| 1236 | * );
|
| 1237 | * }
|
| 1238 | * ```
|
| 1239 | *
|
| 1240 | * @public
|
| 1241 | * @category Hooks
|
| 1242 | * @mode framework
|
| 1243 | * @mode data
|
| 1244 | * @param shouldBlock Either a boolean or a function returning a boolean which
|
| 1245 | * indicates whether the navigation should be blocked. The function format
|
| 1246 | * receives a single object parameter containing the `currentLocation`,
|
| 1247 | * `nextLocation`, and `historyAction` of the potential navigation.
|
| 1248 | * @returns A {@link Blocker} object with state and reset functionality
|
| 1249 | */
|
| 1250 | function useBlocker(shouldBlock) {
|
| 1251 | let { router, basename } = useDataRouterContext("useBlocker");
|
| 1252 | let state = useDataRouterState("useBlocker");
|
| 1253 | let [blockerKey, setBlockerKey] = React$1.useState("");
|
| 1254 | let blockerFunction = React$1.useCallback((arg) => {
|
| 1255 | if (typeof shouldBlock !== "function") return !!shouldBlock;
|
| 1256 | if (basename === "/") return shouldBlock(arg);
|
| 1257 | let { currentLocation, nextLocation, historyAction } = arg;
|
| 1258 | return shouldBlock({
|
| 1259 | currentLocation: {
|
| 1260 | ...currentLocation,
|
| 1261 | pathname: stripBasename(currentLocation.pathname, basename) || currentLocation.pathname
|
| 1262 | },
|
| 1263 | nextLocation: {
|
| 1264 | ...nextLocation,
|
| 1265 | pathname: stripBasename(nextLocation.pathname, basename) || nextLocation.pathname
|
| 1266 | },
|
| 1267 | historyAction
|
| 1268 | });
|
| 1269 | }, [basename, shouldBlock]);
|
| 1270 | React$1.useEffect(() => {
|
| 1271 | let key = String(++blockerId);
|
| 1272 | setBlockerKey(key);
|
| 1273 | return () => router.deleteBlocker(key);
|
| 1274 | }, [router]);
|
| 1275 | React$1.useEffect(() => {
|
| 1276 | if (blockerKey !== "") router.getBlocker(blockerKey, blockerFunction);
|
| 1277 | }, [
|
| 1278 | router,
|
| 1279 | blockerKey,
|
| 1280 | blockerFunction
|
| 1281 | ]);
|
| 1282 | return blockerKey && state.blockers.has(blockerKey) ? state.blockers.get(blockerKey) : IDLE_BLOCKER;
|
| 1283 | }
|
| 1284 | function useNavigateStable() {
|
| 1285 | let { router } = useDataRouterContext("useNavigate");
|
| 1286 | let id = useCurrentRouteId("useNavigate");
|
| 1287 | let activeRef = React$1.useRef(false);
|
| 1288 | React$1.useLayoutEffect(() => {
|
| 1289 | activeRef.current = true;
|
| 1290 | });
|
| 1291 | return React$1.useCallback(async (to, options = {}) => {
|
| 1292 | warning(activeRef.current, navigateEffectWarning);
|
| 1293 | if (!activeRef.current) return;
|
| 1294 | if (typeof to === "number") await router.navigate(to);
|
| 1295 | else await router.navigate(to, {
|
| 1296 | fromRouteId: id,
|
| 1297 | ...options
|
| 1298 | });
|
| 1299 | }, [router, id]);
|
| 1300 | }
|
| 1301 | const alreadyWarned = {};
|
| 1302 | function warningOnce(key, cond, message) {
|
| 1303 | if (!cond && !alreadyWarned[key]) {
|
| 1304 | alreadyWarned[key] = true;
|
| 1305 | warning(false, message);
|
| 1306 | }
|
| 1307 | }
|
| 1308 | function useRoute(...args) {
|
| 1309 | const currentRouteId = useCurrentRouteId("useRoute");
|
| 1310 | const id = args[0] ?? currentRouteId;
|
| 1311 | const state = useDataRouterState("useRoute");
|
| 1312 | const route = state.matches.find(({ route }) => route.id === id);
|
| 1313 | if (route === void 0) return void 0;
|
| 1314 | return {
|
| 1315 | handle: route.route.handle,
|
| 1316 | loaderData: state.loaderData[id],
|
| 1317 | actionData: state.actionData?.[id]
|
| 1318 | };
|
| 1319 | }
|
| 1320 | function toRouterStateMatch(match) {
|
| 1321 | return {
|
| 1322 | id: match.route.id,
|
| 1323 | pathname: match.pathname,
|
| 1324 | params: match.params,
|
| 1325 | handle: match.route.handle
|
| 1326 | };
|
| 1327 | }
|
| 1328 | /**
|
| 1329 | * A unified hook for reading router state: current (`active`) and in-flight
|
| 1330 | * (`pending`) locations, search params, params, matches, and navigation type.
|
| 1331 | *
|
| 1332 | * This hook consolidates the information you used to get from {@link useLocation},
|
| 1333 | * {@link useSearchParams}, {@link useParams}, {@link useMatches}, {@link useNavigation},
|
| 1334 | * and {@link useNavigationType} into a single hook.
|
| 1335 | *
|
| 1336 | *
|
| 1337 | * @example
|
| 1338 | * import { unstable_useRouterState as useRouterState } from "react-router";
|
| 1339 | *
|
| 1340 | * let { active, pending } = unstable_useRouterState();
|
| 1341 | *
|
| 1342 | * // Active is always populated with the current location
|
| 1343 | * active.location; // replaces `useLocation()`
|
| 1344 | * active.searchParams; // replaces `useSearchParams()[0]`
|
| 1345 | * active.params; // replaces `useParams()`
|
| 1346 | * active.matches; // replaces `useMatches()`
|
| 1347 | * active.type; // replaces `useNavigationType()`
|
| 1348 | *
|
| 1349 | * // Pending is only populated during a navigation
|
| 1350 | * pending.location; // replaces `useNavigation().location`
|
| 1351 | * pending.searchParams; // equivalent to `new URLSearchParams(useNavigation().search)`
|
| 1352 | * pending.params; // Not directly accessible today
|
| 1353 | * pending.matches; // Not directly accessible today
|
| 1354 | * pending.type; // Not directly accessible today
|
| 1355 | * pending.state; // replaces `useNavigation().state`
|
| 1356 | * pending.formMethod; // replaces useNavigation().formMethod
|
| 1357 | * pending.formAction; // replaces useNavigation().formAction
|
| 1358 | * pending.formEncType; // replaces useNavigation().formEncType
|
| 1359 | * pending.formData; // replaces useNavigation().formData
|
| 1360 | * pending.json; // replaces useNavigation().json
|
| 1361 | * pending.text; // replaces useNavigation().text
|
| 1362 | *
|
| 1363 | * @name unstable_useRouterState
|
| 1364 | * @public
|
| 1365 | * @category Hooks
|
| 1366 | * @mode framework
|
| 1367 | * @mode data
|
| 1368 | * @returns The current router state with `active` and `pending` variants
|
| 1369 | */
|
| 1370 | function useRouterState() {
|
| 1371 | let { location, historyAction: type, matches, navigation } = useDataRouterState("unstable_useRouterState");
|
| 1372 | let active = React$1.useMemo(() => ({
|
| 1373 | type,
|
| 1374 | location,
|
| 1375 | searchParams: new URLSearchParams(location.search),
|
| 1376 | params: matches[matches.length - 1]?.params ?? {},
|
| 1377 | matches: matches.map((m) => toRouterStateMatch(m))
|
| 1378 | }), [
|
| 1379 | location,
|
| 1380 | matches,
|
| 1381 | type
|
| 1382 | ]);
|
| 1383 | let pending = React$1.useMemo(() => {
|
| 1384 | if (navigation.state === "idle") return null;
|
| 1385 | let shared = {
|
| 1386 | type: navigation.historyAction,
|
| 1387 | location: navigation.location,
|
| 1388 | searchParams: new URLSearchParams(navigation.location.search),
|
| 1389 | params: navigation.matches[navigation.matches.length - 1]?.params ?? {},
|
| 1390 | matches: navigation.matches.map((m) => toRouterStateMatch(m))
|
| 1391 | };
|
| 1392 | return navigation.state === "loading" ? {
|
| 1393 | ...shared,
|
| 1394 | state: "loading",
|
| 1395 | formMethod: navigation.formMethod,
|
| 1396 | formAction: navigation.formAction,
|
| 1397 | formEncType: navigation.formEncType,
|
| 1398 | formData: navigation.formData,
|
| 1399 | json: navigation.json,
|
| 1400 | text: navigation.text
|
| 1401 | } : {
|
| 1402 | ...shared,
|
| 1403 | state: "submitting",
|
| 1404 | formMethod: navigation.formMethod,
|
| 1405 | formAction: navigation.formAction,
|
| 1406 | formEncType: navigation.formEncType,
|
| 1407 | formData: navigation.formData,
|
| 1408 | json: navigation.json,
|
| 1409 | text: navigation.text
|
| 1410 | };
|
| 1411 | }, [navigation]);
|
| 1412 | return React$1.useMemo(() => ({
|
| 1413 | active,
|
| 1414 | pending
|
| 1415 | }), [active, pending]);
|
| 1416 | }
|
| 1417 | //#endregion
|
| 1418 | export { _renderMatches, useActionData, useAsyncError, useAsyncValue, useBlocker, useHref, useInRouterContext, useLoaderData, useLocation, useMatch, useMatches, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRevalidator, useRoute, useRouteError, useRouteId, useRouteLoaderData, useRouterState, useRoutes, useRoutesImpl };
|