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