@octanejs/remix-router 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -0
  3. package/package.json +53 -0
  4. package/src/dom.ts +16 -0
  5. package/src/index.ts +267 -0
  6. package/src/internal.ts +37 -0
  7. package/src/lib/Await.tsrx +145 -0
  8. package/src/lib/Await.tsrx.d.ts +6 -0
  9. package/src/lib/DefaultErrorComponent.tsrx +47 -0
  10. package/src/lib/DefaultErrorComponent.tsrx.d.ts +2 -0
  11. package/src/lib/RenderErrorBoundary.tsrx +117 -0
  12. package/src/lib/RenderErrorBoundary.tsrx.d.ts +14 -0
  13. package/src/lib/actions.ts +90 -0
  14. package/src/lib/components/MemoryRouter.tsrx +42 -0
  15. package/src/lib/components/MemoryRouter.tsrx.d.ts +10 -0
  16. package/src/lib/components/Navigate.ts +60 -0
  17. package/src/lib/components/Router.tsrx +88 -0
  18. package/src/lib/components/Router.tsrx.d.ts +13 -0
  19. package/src/lib/components/RouterProvider.tsrx +320 -0
  20. package/src/lib/components/RouterProvider.tsrx.d.ts +21 -0
  21. package/src/lib/components/Routes.tsrx +53 -0
  22. package/src/lib/components/Routes.tsrx.d.ts +7 -0
  23. package/src/lib/components/routes-collector.ts +317 -0
  24. package/src/lib/components/utils.ts +171 -0
  25. package/src/lib/components/with-props.ts +72 -0
  26. package/src/lib/context.ts +129 -0
  27. package/src/lib/dom/Form.tsrx +76 -0
  28. package/src/lib/dom/Form.tsrx.d.ts +22 -0
  29. package/src/lib/dom/Link.tsrx +91 -0
  30. package/src/lib/dom/Link.tsrx.d.ts +22 -0
  31. package/src/lib/dom/NavLink.tsrx +118 -0
  32. package/src/lib/dom/NavLink.tsrx.d.ts +33 -0
  33. package/src/lib/dom/dom.ts +344 -0
  34. package/src/lib/dom/hooks.ts +93 -0
  35. package/src/lib/dom/lib.ts +822 -0
  36. package/src/lib/dom/routers.tsrx +104 -0
  37. package/src/lib/dom/routers.tsrx.d.ts +23 -0
  38. package/src/lib/dom/server.tsrx +350 -0
  39. package/src/lib/dom/server.tsrx.d.ts +25 -0
  40. package/src/lib/errors.ts +84 -0
  41. package/src/lib/framework-stubs.ts +103 -0
  42. package/src/lib/hooks.ts +1797 -0
  43. package/src/lib/href.ts +71 -0
  44. package/src/lib/react-types.ts +10 -0
  45. package/src/lib/router/history.ts +763 -0
  46. package/src/lib/router/instrumentation.ts +496 -0
  47. package/src/lib/router/links.ts +192 -0
  48. package/src/lib/router/router.ts +7165 -0
  49. package/src/lib/router/server-runtime-types.ts +12 -0
  50. package/src/lib/router/url.ts +8 -0
  51. package/src/lib/router/utils.ts +2179 -0
  52. package/src/lib/server-runtime/cookies.ts +240 -0
  53. package/src/lib/server-runtime/crypto.ts +55 -0
  54. package/src/lib/server-runtime/mode.ts +16 -0
  55. package/src/lib/server-runtime/sessions/cookieStorage.ts +54 -0
  56. package/src/lib/server-runtime/sessions/memoryStorage.ts +59 -0
  57. package/src/lib/server-runtime/sessions.ts +291 -0
  58. package/src/lib/server-runtime/warnings.ts +10 -0
  59. package/src/lib/types/future.ts +16 -0
  60. package/src/lib/types/params.ts +8 -0
  61. package/src/lib/types/register.ts +39 -0
  62. package/src/lib/types/route-module.ts +17 -0
  63. package/src/lib/types/utils.ts +37 -0
@@ -0,0 +1,1797 @@
1
+ // Transcribed from react-router@7.18.1 lib/hooks.tsx onto octane. Upstream
2
+ // logic, comments, and warning strings are verbatim; octane substitutions:
3
+ // hooks from 'octane' with the binding slot idiom (splitSlot/subSlot — this
4
+ // file is NOT compiled, so exported hooks peel the caller's compiler-injected
5
+ // slot off their trailing args and derive a distinct sub-slot per base-hook
6
+ // call site), JSX → createElement descriptors (plain .ts), `__DEV__` →
7
+ // ENABLE_DEV_WARNINGS, and the class RenderErrorBoundary / inline
8
+ // DefaultErrorComponent live in sibling .tsrx files. RSC branches are dropped
9
+ // at their upstream sites with OCTANE notes.
10
+ import {
11
+ createContext,
12
+ createElement,
13
+ useCallback,
14
+ useContext,
15
+ useEffect,
16
+ useLayoutEffect,
17
+ useMemo,
18
+ useRef,
19
+ useState,
20
+ } from 'octane';
21
+ import type { NavigateOptions, RouteContextObject, ClientOnErrorFunction } from './context';
22
+ import {
23
+ AwaitContext,
24
+ DataRouterContext,
25
+ DataRouterStateContext,
26
+ ENABLE_DEV_WARNINGS,
27
+ LocationContext,
28
+ NavigationContext,
29
+ RouteContext,
30
+ RouteErrorContext,
31
+ } from './context';
32
+ import type { Location, Path, To } from './router/history';
33
+ import { Action as NavigationType, invariant, parsePath, warning } from './router/history';
34
+ import type {
35
+ Blocker,
36
+ BlockerFunction,
37
+ RelativeRoutingType,
38
+ Router as DataRouter,
39
+ NavigationStates,
40
+ } from './router/router';
41
+ import { IDLE_BLOCKER } from './router/router';
42
+ // OCTANE: dropped — upstream also imports hasInvalidProtocol here; it serves
43
+ // the RSC redirect handler (no octane RSC runtime).
44
+ import type {
45
+ DataRouteMatch,
46
+ ParamParseKey,
47
+ Params,
48
+ PathMatch,
49
+ PathPattern,
50
+ RouteManifest,
51
+ RouteMatch,
52
+ RouteObject,
53
+ UIMatch,
54
+ } from './router/utils';
55
+ import {
56
+ convertRouteMatchToUiMatch,
57
+ decodePath,
58
+ getResolveToMatches,
59
+ getRoutePattern,
60
+ joinPaths,
61
+ matchPath,
62
+ matchRoutes,
63
+ resolveTo,
64
+ stripBasename,
65
+ } from './router/utils';
66
+ // OCTANE: dropped — upstream imports { GetActionData, GetLoaderData,
67
+ // SerializeFrom } from ./types/route-data (framework-mode serialization
68
+ // typing, not vendored) and RouteModules from ./types/register; they serve
69
+ // useRoute (Phase E) and the SerializeFrom generic, declared loosely below.
70
+ // OCTANE: dropped — upstream imports { decodeRedirectErrorDigest,
71
+ // decodeRouteErrorResponseDigest } from ./errors; both are consumed only by
72
+ // the RSC digest branches (RenderErrorBoundary.render / RSCErrorHandler).
73
+ import { RenderErrorBoundary } from './RenderErrorBoundary.tsrx';
74
+ import { DefaultErrorComponent } from './DefaultErrorComponent.tsrx';
75
+ import { splitSlot, subSlot } from '../internal';
76
+
77
+ /**
78
+ * Resolves a URL against the current {@link Location}.
79
+ *
80
+ * @example
81
+ * import { useHref } from "react-router";
82
+ *
83
+ * function SomeComponent() {
84
+ * let href = useHref("some/where");
85
+ * // "/resolved/some/where"
86
+ * }
87
+ *
88
+ * @public
89
+ * @category Hooks
90
+ * @param to The path to resolve
91
+ * @param options Options
92
+ * @param options.relative Defaults to `"route"` so routing is relative to the
93
+ * route tree.
94
+ * Set to `"path"` to make relative routing operate against path segments.
95
+ * @returns The resolved href string
96
+ */
97
+ export function useHref(to: To, ...args: any[]): string {
98
+ const [user, slot] = splitSlot(args);
99
+ let { relative } = (user[0] as { relative?: RelativeRoutingType } | undefined) ?? {};
100
+ invariant(
101
+ useInRouterContext(),
102
+ // TODO: This error is probably because they somehow have 2 versions of the
103
+ // router loaded. We can help them understand how to avoid that.
104
+ `useHref() may be used only in the context of a <Router> component.`,
105
+ );
106
+
107
+ let { basename, navigator } = useContext(NavigationContext);
108
+ let { hash, pathname, search } = useResolvedPath(to, { relative }, subSlot(slot, 'href:rp'));
109
+
110
+ let joinedPathname = pathname;
111
+
112
+ // If we're operating within a basename, prepend it to the pathname prior
113
+ // to creating the href. If this is a root navigation, then just use the raw
114
+ // basename which allows the basename to have full control over the presence
115
+ // of a trailing slash on root links
116
+ if (basename !== '/') {
117
+ joinedPathname = pathname === '/' ? basename : joinPaths([basename, pathname]);
118
+ }
119
+
120
+ return navigator.createHref({ pathname: joinedPathname, search, hash });
121
+ }
122
+
123
+ /**
124
+ * Returns `true` if this component is a descendant of a {@link Router}, useful
125
+ * to ensure a component is used within a {@link Router}.
126
+ *
127
+ * @public
128
+ * @category Hooks
129
+ * @mode framework
130
+ * @mode data
131
+ * @returns Whether the component is within a {@link Router} context
132
+ */
133
+ export function useInRouterContext(): boolean {
134
+ return useContext(LocationContext) != null;
135
+ }
136
+
137
+ /**
138
+ * Returns the current {@link Location}. This can be useful if you'd like to
139
+ * perform some side effect whenever it changes.
140
+ *
141
+ * @example
142
+ * import { useLocation } from 'react-router'
143
+ *
144
+ * function SomeComponent() {
145
+ * let location = useLocation()
146
+ *
147
+ * useEffect(() => {
148
+ * // Google Analytics
149
+ * ga('send', 'pageview')
150
+ * }, [location]);
151
+ *
152
+ * return (
153
+ * // ...
154
+ * );
155
+ * }
156
+ *
157
+ * @public
158
+ * @category Hooks
159
+ * @returns The current {@link Location} object
160
+ */
161
+ export function useLocation(): Location {
162
+ invariant(
163
+ useInRouterContext(),
164
+ // TODO: This error is probably because they somehow have 2 versions of the
165
+ // router loaded. We can help them understand how to avoid that.
166
+ `useLocation() may be used only in the context of a <Router> component.`,
167
+ );
168
+
169
+ return useContext(LocationContext).location;
170
+ }
171
+
172
+ /**
173
+ * Returns the current {@link Navigation} action which describes how the router
174
+ * came to the current {@link Location}, either by a pop, push, or replace on
175
+ * the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) stack.
176
+ *
177
+ * @public
178
+ * @category Hooks
179
+ * @returns The current {@link NavigationType} (`"POP"`, `"PUSH"`, or `"REPLACE"`)
180
+ */
181
+ export function useNavigationType(): NavigationType {
182
+ return useContext(LocationContext).navigationType;
183
+ }
184
+
185
+ /**
186
+ * Returns a {@link PathMatch} object if the given pattern matches the current URL.
187
+ * This is useful for components that need to know "active" state, e.g.
188
+ * {@link NavLink | `<NavLink>`}.
189
+ *
190
+ * @public
191
+ * @category Hooks
192
+ * @param pattern The pattern to match against the current {@link Location}
193
+ * @returns The path match object if the pattern matches, `null` otherwise
194
+ */
195
+ export function useMatch<Path extends string>(
196
+ pattern: PathPattern<Path> | Path,
197
+ ...args: any[]
198
+ ): PathMatch<ParamParseKey<Path>> | null {
199
+ const [, slot] = splitSlot(args);
200
+ invariant(
201
+ useInRouterContext(),
202
+ // TODO: This error is probably because they somehow have 2 versions of the
203
+ // router loaded. We can help them understand how to avoid that.
204
+ `useMatch() may be used only in the context of a <Router> component.`,
205
+ );
206
+
207
+ let { pathname } = useLocation();
208
+ return useMemo(
209
+ () => matchPath<Path>(pattern, decodePath(pathname)),
210
+ [pathname, pattern],
211
+ subSlot(slot, 'match:memo'),
212
+ );
213
+ }
214
+
215
+ /**
216
+ * The interface for the `navigate` function returned from {@link useNavigate}.
217
+ */
218
+ export interface NavigateFunction {
219
+ (to: To, options?: NavigateOptions): void | Promise<void>;
220
+ (delta: number): void | Promise<void>;
221
+ }
222
+
223
+ const navigateEffectWarning =
224
+ `You should call navigate() in a useEffect(), not when ` + `your component is first rendered.`;
225
+
226
+ // Mute warnings for calls to useNavigate in SSR environments
227
+ function useIsomorphicLayoutEffect(cb: Parameters<typeof useLayoutEffect>[0], slot?: symbol) {
228
+ let isStatic = useContext(NavigationContext).static;
229
+ if (!isStatic) {
230
+ // We should be able to get rid of this once react 18.3 is released
231
+ // See: https://github.com/facebook/react/pull/26395
232
+ // OCTANE: conditional hook calls are fine — hooks are slot-keyed, not
233
+ // call-order keyed.
234
+ useLayoutEffect(cb, undefined, subSlot(slot, 'ile'));
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Returns a function that lets you navigate programmatically in the browser in
240
+ * response to user interactions or effects.
241
+ *
242
+ * It's often better to use {@link redirect} in [`action`](../../start/framework/route-module#action)/[`loader`](../../start/framework/route-module#loader)
243
+ * functions than this hook.
244
+ *
245
+ * The returned function signature is `navigate(to, options?)`/`navigate(delta)` where:
246
+ *
247
+ * * `to` can be a string path, a {@link To} object, or a number (delta)
248
+ * * `options` contains options for modifying the navigation
249
+ * * These options work in all modes (Framework, Data, and Declarative):
250
+ * * `relative`: `"route"` or `"path"` to control relative routing logic
251
+ * * `replace`: Replace the current entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) stack
252
+ * * `state`: Optional [`history.state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) to include with the new {@link Location}
253
+ * * These options only work in Framework and Data modes:
254
+ * * `flushSync`: Wrap the DOM updates in [`ReactDom.flushSync`](https://react.dev/reference/react-dom/flushSync)
255
+ * * `preventScrollReset`: Do not scroll back to the top of the page after navigation
256
+ * * `viewTransition`: Enable [`document.startViewTransition`](https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition) for this navigation
257
+ *
258
+ * @example
259
+ * import { useNavigate } from "react-router";
260
+ *
261
+ * function SomeComponent() {
262
+ * let navigate = useNavigate();
263
+ * return (
264
+ * <button onClick={() => navigate(-1)}>
265
+ * Go Back
266
+ * </button>
267
+ * );
268
+ * }
269
+ *
270
+ * @additionalExamples
271
+ * ### Navigate to another path
272
+ *
273
+ * ```tsx
274
+ * navigate("/some/route");
275
+ * navigate("/some/route?search=param");
276
+ * ```
277
+ *
278
+ * ### Navigate with a {@link To} object
279
+ *
280
+ * All properties are optional.
281
+ *
282
+ * ```tsx
283
+ * navigate({
284
+ * pathname: "/some/route",
285
+ * search: "?search=param",
286
+ * hash: "#hash",
287
+ * state: { some: "state" },
288
+ * });
289
+ * ```
290
+ *
291
+ * If you use `state`, that will be available on the {@link Location} object on
292
+ * the next page. Access it with `useLocation().state` (see {@link useLocation}).
293
+ *
294
+ * ### Navigate back or forward in the history stack
295
+ *
296
+ * ```tsx
297
+ * // back
298
+ * // often used to close modals
299
+ * navigate(-1);
300
+ *
301
+ * // forward
302
+ * // often used in a multistep wizard workflows
303
+ * navigate(1);
304
+ * ```
305
+ *
306
+ * Be cautious with `navigate(number)`. If your application can load up to a
307
+ * route that has a button that tries to navigate forward/back, there may not be
308
+ * a [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)
309
+ * entry to go back or forward to, or it can go somewhere you don't expect
310
+ * (like a different domain).
311
+ *
312
+ * 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)
313
+ * stack to navigate to.
314
+ *
315
+ * ### Replace the current entry in the history stack
316
+ *
317
+ * This will remove the current entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)
318
+ * stack, replacing it with a new one, similar to a server side redirect.
319
+ *
320
+ * ```tsx
321
+ * navigate("/some/route", { replace: true });
322
+ * ```
323
+ *
324
+ * ### Prevent Scroll Reset
325
+ *
326
+ * [MODES: framework, data]
327
+ *
328
+ * <br/>
329
+ * <br/>
330
+ *
331
+ * To prevent {@link ScrollRestoration | `<ScrollRestoration>`} from resetting
332
+ * the scroll position, use the `preventScrollReset` option.
333
+ *
334
+ * ```tsx
335
+ * navigate("?some-tab=1", { preventScrollReset: true });
336
+ * ```
337
+ *
338
+ * For example, if you have a tab interface connected to search params in the
339
+ * middle of a page, and you don't want it to scroll to the top when a tab is
340
+ * clicked.
341
+ *
342
+ * ### Return Type Augmentation
343
+ *
344
+ * Internally, `useNavigate` uses a separate implementation when you are in
345
+ * Declarative mode versus Data/Framework mode - the primary difference being
346
+ * that the latter is able to return a stable reference that does not change
347
+ * identity across navigations. The implementation in Data/Framework mode also
348
+ * returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
349
+ * that resolves when the navigation is completed. This means the return type of
350
+ * `useNavigate` is `void | Promise<void>`. This is accurate, but can lead to
351
+ * some red squigglies based on the union in the return value:
352
+ *
353
+ * - If you're using `typescript-eslint`, you may see errors from
354
+ * [`@typescript-eslint/no-floating-promises`](https://typescript-eslint.io/rules/no-floating-promises)
355
+ * - In Framework/Data mode, `React.use(navigate())` will show a false-positive
356
+ * `Argument of type 'void | Promise<void>' is not assignable to parameter of
357
+ * type 'Usable<void>'` error
358
+ *
359
+ * The easiest way to work around these issues is to augment the type based on the
360
+ * router you're using:
361
+ *
362
+ * ```ts
363
+ * // If using <BrowserRouter>
364
+ * declare module "react-router" {
365
+ * interface NavigateFunction {
366
+ * (to: To, options?: NavigateOptions): void;
367
+ * (delta: number): void;
368
+ * }
369
+ * }
370
+ *
371
+ * // If using <RouterProvider> or Framework mode
372
+ * declare module "react-router" {
373
+ * interface NavigateFunction {
374
+ * (to: To, options?: NavigateOptions): Promise<void>;
375
+ * (delta: number): Promise<void>;
376
+ * }
377
+ * }
378
+ * ```
379
+ *
380
+ * @public
381
+ * @category Hooks
382
+ * @returns A navigate function for programmatic navigation
383
+ */
384
+ export function useNavigate(...args: any[]): NavigateFunction {
385
+ const [, slot] = splitSlot(args);
386
+ let { isDataRoute } = useContext(RouteContext);
387
+ // Conditional usage is OK here because the usage of a data router is static
388
+ return isDataRoute
389
+ ? useNavigateStable(subSlot(slot, 'nav:stable'))
390
+ : useNavigateUnstable(subSlot(slot, 'nav:unstable'));
391
+ }
392
+
393
+ function useNavigateUnstable(slot?: symbol): NavigateFunction {
394
+ invariant(
395
+ useInRouterContext(),
396
+ // TODO: This error is probably because they somehow have 2 versions of the
397
+ // router loaded. We can help them understand how to avoid that.
398
+ `useNavigate() may be used only in the context of a <Router> component.`,
399
+ );
400
+
401
+ let dataRouterContext = useContext(DataRouterContext);
402
+ let { basename, navigator } = useContext(NavigationContext);
403
+ let { matches } = useContext(RouteContext);
404
+ let { pathname: locationPathname } = useLocation();
405
+
406
+ let routePathnamesJson = JSON.stringify(getResolveToMatches(matches));
407
+
408
+ let activeRef = useRef(false, subSlot(slot, 'unav:ref'));
409
+ useIsomorphicLayoutEffect(
410
+ () => {
411
+ activeRef.current = true;
412
+ },
413
+ subSlot(slot, 'unav:ile'),
414
+ );
415
+
416
+ let navigate: NavigateFunction = useCallback(
417
+ (to: To | number, options: NavigateOptions = {}) => {
418
+ warning(activeRef.current, navigateEffectWarning);
419
+
420
+ // Short circuit here since if this happens on first render the navigate
421
+ // is useless because we haven't wired up our history listener yet
422
+ if (!activeRef.current) return;
423
+
424
+ if (typeof to === 'number') {
425
+ navigator.go(to);
426
+ return;
427
+ }
428
+
429
+ let path = resolveTo(
430
+ to,
431
+ JSON.parse(routePathnamesJson),
432
+ locationPathname,
433
+ options.relative === 'path',
434
+ );
435
+
436
+ // If we're operating within a basename, prepend it to the pathname prior
437
+ // to handing off to history (but only if we're not in a data router,
438
+ // otherwise it'll prepend the basename inside of the router).
439
+ // If this is a root navigation, then we navigate to the raw basename
440
+ // which allows the basename to have full control over the presence of a
441
+ // trailing slash on root links
442
+ if (dataRouterContext == null && basename !== '/') {
443
+ path.pathname = path.pathname === '/' ? basename : joinPaths([basename, path.pathname]);
444
+ }
445
+
446
+ (!!options.replace ? navigator.replace : navigator.push)(path, options.state, options);
447
+ },
448
+ [basename, navigator, routePathnamesJson, locationPathname, dataRouterContext],
449
+ subSlot(slot, 'unav:cb'),
450
+ );
451
+
452
+ return navigate;
453
+ }
454
+
455
+ const OutletContext = createContext<unknown>(null);
456
+
457
+ /**
458
+ * Returns the parent route {@link Outlet | `<Outlet context>`}.
459
+ *
460
+ * Often parent routes manage state or other values you want shared with child
461
+ * routes. You can create your own [context provider](https://react.dev/learn/passing-data-deeply-with-context)
462
+ * if you like, but this is such a common situation that it's built-into
463
+ * {@link Outlet | `<Outlet>`}.
464
+ *
465
+ * ```tsx
466
+ * // Parent route
467
+ * function Parent() {
468
+ * const [count, setCount] = useState(0);
469
+ * return <Outlet context={[count, setCount]} />;
470
+ * }
471
+ * ```
472
+ *
473
+ * ```tsx
474
+ * // Child route
475
+ * import { useOutletContext } from "react-router";
476
+ *
477
+ * function Child() {
478
+ * const [count, setCount] = useOutletContext();
479
+ * const increment = () => setCount((c) => c + 1);
480
+ * return <button onClick={increment}>{count}</button>;
481
+ * }
482
+ * ```
483
+ *
484
+ * If you're using TypeScript, we recommend the parent component provide a
485
+ * custom hook for accessing the context value. This makes it easier for
486
+ * consumers to get nice typings, control consumers, and know who's consuming
487
+ * the context value.
488
+ *
489
+ * Here's a more realistic example:
490
+ *
491
+ * ```tsx filename=src/routes/dashboard.tsx lines=[14,20]
492
+ * import { useState } from "react";
493
+ * import { Outlet, useOutletContext } from "react-router";
494
+ *
495
+ * import type { User } from "./types";
496
+ *
497
+ * type ContextType = { user: User | null };
498
+ *
499
+ * export default function Dashboard() {
500
+ * const [user, setUser] = useState<User | null>(null);
501
+ *
502
+ * return (
503
+ * <div>
504
+ * <h1>Dashboard</h1>
505
+ * <Outlet context={{ user } satisfies ContextType} />
506
+ * </div>
507
+ * );
508
+ * }
509
+ *
510
+ * export function useUser() {
511
+ * return useOutletContext<ContextType>();
512
+ * }
513
+ * ```
514
+ *
515
+ * ```tsx filename=src/routes/dashboard/messages.tsx lines=[1,4]
516
+ * import { useUser } from "../dashboard";
517
+ *
518
+ * export default function DashboardMessages() {
519
+ * const { user } = useUser();
520
+ * return (
521
+ * <div>
522
+ * <h2>Messages</h2>
523
+ * <p>Hello, {user.name}!</p>
524
+ * </div>
525
+ * );
526
+ * }
527
+ * ```
528
+ *
529
+ * @public
530
+ * @category Hooks
531
+ * @returns The context value passed to the parent {@link Outlet} component
532
+ */
533
+ export function useOutletContext<Context = unknown>(): Context {
534
+ return useContext(OutletContext) as Context;
535
+ }
536
+
537
+ /**
538
+ * Returns the element for the child route at this level of the route
539
+ * hierarchy. Used internally by {@link Outlet | `<Outlet>`} to render child
540
+ * routes.
541
+ *
542
+ * @public
543
+ * @category Hooks
544
+ * @param context The context to pass to the outlet
545
+ * @returns The child route element or `null` if no child routes match
546
+ */
547
+ export function useOutlet(...args: any[]): unknown {
548
+ const [user, slot] = splitSlot(args);
549
+ let context = user[0] as unknown;
550
+ let outlet = useContext(RouteContext).outlet;
551
+ return useMemo(
552
+ () =>
553
+ outlet && createElement(OutletContext.Provider as any, { value: context, children: outlet }),
554
+ [outlet, context],
555
+ subSlot(slot, 'outlet:memo'),
556
+ );
557
+ }
558
+
559
+ /**
560
+ * Returns an object of key/value-pairs of the dynamic params from the current
561
+ * URL that were matched by the routes. Child routes inherit all params from
562
+ * their parent routes.
563
+ *
564
+ * Assuming a route pattern like `/posts/:postId` is matched by `/posts/123`
565
+ * then `params.postId` will be `"123"`.
566
+ *
567
+ * @example
568
+ * import { useParams } from "react-router";
569
+ *
570
+ * function SomeComponent() {
571
+ * let params = useParams();
572
+ * params.postId;
573
+ * }
574
+ *
575
+ * @additionalExamples
576
+ * ### Basic Usage
577
+ *
578
+ * ```tsx
579
+ * import { useParams } from "react-router";
580
+ *
581
+ * // given a route like:
582
+ * <Route path="/posts/:postId" element={<Post />} />;
583
+ *
584
+ * // or a data route like:
585
+ * createBrowserRouter([
586
+ * {
587
+ * path: "/posts/:postId",
588
+ * component: Post,
589
+ * },
590
+ * ]);
591
+ *
592
+ * // or in routes.ts
593
+ * route("/posts/:postId", "routes/post.tsx");
594
+ * ```
595
+ *
596
+ * Access the params in a component:
597
+ *
598
+ * ```tsx
599
+ * import { useParams } from "react-router";
600
+ *
601
+ * export default function Post() {
602
+ * let params = useParams();
603
+ * return <h1>Post: {params.postId}</h1>;
604
+ * }
605
+ * ```
606
+ *
607
+ * ### Multiple Params
608
+ *
609
+ * Patterns can have multiple params:
610
+ *
611
+ * ```tsx
612
+ * "/posts/:postId/comments/:commentId";
613
+ * ```
614
+ *
615
+ * All will be available in the params object:
616
+ *
617
+ * ```tsx
618
+ * import { useParams } from "react-router";
619
+ *
620
+ * export default function Post() {
621
+ * let params = useParams();
622
+ * return (
623
+ * <h1>
624
+ * Post: {params.postId}, Comment: {params.commentId}
625
+ * </h1>
626
+ * );
627
+ * }
628
+ * ```
629
+ *
630
+ * ### Catchall Params
631
+ *
632
+ * Catchall params are defined with `*`:
633
+ *
634
+ * ```tsx
635
+ * "/files/*";
636
+ * ```
637
+ *
638
+ * The matched value will be available in the params object as follows:
639
+ *
640
+ * ```tsx
641
+ * import { useParams } from "react-router";
642
+ *
643
+ * export default function File() {
644
+ * let params = useParams();
645
+ * let catchall = params["*"];
646
+ * // ...
647
+ * }
648
+ * ```
649
+ *
650
+ * You can destructure the catchall param:
651
+ *
652
+ * ```tsx
653
+ * export default function File() {
654
+ * let { "*": catchall } = useParams();
655
+ * console.log(catchall);
656
+ * }
657
+ * ```
658
+ *
659
+ * @public
660
+ * @category Hooks
661
+ * @returns An object containing the dynamic route parameters
662
+ */
663
+ export function useParams<
664
+ ParamsOrKey extends string | Record<string, string | undefined> = string,
665
+ >(): Readonly<[ParamsOrKey] extends [string] ? Params<ParamsOrKey> : Partial<ParamsOrKey>> {
666
+ let { matches } = useContext(RouteContext);
667
+ let routeMatch = matches[matches.length - 1];
668
+ return (routeMatch?.params ?? {}) as any;
669
+ }
670
+
671
+ /**
672
+ * Resolves the pathname of the given `to` value against the current
673
+ * {@link Location}. Similar to {@link useHref}, but returns a
674
+ * {@link Path} instead of a string.
675
+ *
676
+ * @example
677
+ * import { useResolvedPath } from "react-router";
678
+ *
679
+ * function SomeComponent() {
680
+ * // if the user is at /dashboard/profile
681
+ * let path = useResolvedPath("../accounts");
682
+ * path.pathname; // "/dashboard/accounts"
683
+ * path.search; // ""
684
+ * path.hash; // ""
685
+ * }
686
+ *
687
+ * @public
688
+ * @category Hooks
689
+ * @param to The path to resolve
690
+ * @param options Options
691
+ * @param options.relative Defaults to `"route"` so routing is relative to the route tree.
692
+ * Set to `"path"` to make relative routing operate against path segments.
693
+ * @returns The resolved {@link Path} object with `pathname`, `search`, and `hash`
694
+ */
695
+ export function useResolvedPath(to: To, ...args: any[]): Path {
696
+ const [user, slot] = splitSlot(args);
697
+ let { relative } = (user[0] as { relative?: RelativeRoutingType } | undefined) ?? {};
698
+ let { matches } = useContext(RouteContext);
699
+ let { pathname: locationPathname } = useLocation();
700
+ let routePathnamesJson = JSON.stringify(getResolveToMatches(matches));
701
+
702
+ return useMemo(
703
+ () => resolveTo(to, JSON.parse(routePathnamesJson), locationPathname, relative === 'path'),
704
+ [to, routePathnamesJson, locationPathname, relative],
705
+ subSlot(slot, 'rp:memo'),
706
+ );
707
+ }
708
+
709
+ /**
710
+ * Hook version of {@link Routes | `<Routes>`} that uses objects instead of
711
+ * components. These objects have the same properties as the component props.
712
+ * The return value of `useRoutes` is either a valid React element you can use
713
+ * to render the route tree, or `null` if nothing matched.
714
+ *
715
+ * @example
716
+ * import { useRoutes } from "react-router";
717
+ *
718
+ * function App() {
719
+ * let element = useRoutes([
720
+ * {
721
+ * path: "/",
722
+ * element: <Dashboard />,
723
+ * children: [
724
+ * {
725
+ * path: "messages",
726
+ * element: <DashboardMessages />,
727
+ * },
728
+ * { path: "tasks", element: <DashboardTasks /> },
729
+ * ],
730
+ * },
731
+ * { path: "team", element: <AboutPage /> },
732
+ * ]);
733
+ *
734
+ * return element;
735
+ * }
736
+ *
737
+ * @public
738
+ * @category Hooks
739
+ * @param routes An array of {@link RouteObject}s that define the route hierarchy
740
+ * @param locationArg An optional {@link Location} object or pathname string to
741
+ * use instead of the current {@link Location}
742
+ * @returns A React element to render the matched route, or `null` if no routes matched
743
+ */
744
+ export function useRoutes(routes: RouteObject[], ...args: any[]): unknown {
745
+ const [user] = splitSlot(args);
746
+ let locationArg = user[0] as Partial<Location> | string | undefined;
747
+ return useRoutesImpl(routes, locationArg);
748
+ }
749
+
750
+ // Internal implementation with accept optional param for RouterProvider usage
751
+ export function useRoutesImpl(routes: RouteObject[], ...args: any[]): unknown {
752
+ const [user] = splitSlot(args);
753
+ let locationArg = user[0] as Partial<Location> | string | undefined;
754
+ let dataRouterOpts = user[1] as
755
+ | {
756
+ manifest: RouteManifest;
757
+ state: DataRouter['state'];
758
+ isStatic: boolean;
759
+ onError: ClientOnErrorFunction | undefined;
760
+ future: DataRouter['future'];
761
+ }
762
+ | undefined;
763
+ invariant(
764
+ useInRouterContext(),
765
+ // TODO: This error is probably because they somehow have 2 versions of the
766
+ // router loaded. We can help them understand how to avoid that.
767
+ `useRoutes() may be used only in the context of a <Router> component.`,
768
+ );
769
+
770
+ let { navigator } = useContext(NavigationContext);
771
+ let { matches: parentMatches } = useContext(RouteContext);
772
+ let routeMatch = parentMatches[parentMatches.length - 1];
773
+ let parentParams = routeMatch ? routeMatch.params : {};
774
+ let parentPathname = routeMatch ? routeMatch.pathname : '/';
775
+ let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : '/';
776
+ let parentRoute = routeMatch && routeMatch.route;
777
+
778
+ if (ENABLE_DEV_WARNINGS) {
779
+ // You won't get a warning about 2 different <Routes> under a <Route>
780
+ // without a trailing *, but this is a best-effort warning anyway since we
781
+ // cannot even give the warning unless they land at the parent route.
782
+ //
783
+ // Example:
784
+ //
785
+ // <Routes>
786
+ // {/* This route path MUST end with /* because otherwise
787
+ // it will never match /blog/post/123 */}
788
+ // <Route path="blog" element={<Blog />} />
789
+ // <Route path="blog/feed" element={<BlogFeed />} />
790
+ // </Routes>
791
+ //
792
+ // function Blog() {
793
+ // return (
794
+ // <Routes>
795
+ // <Route path="post/:id" element={<Post />} />
796
+ // </Routes>
797
+ // );
798
+ // }
799
+ let parentPath = (parentRoute && parentRoute.path) || '';
800
+ warningOnce(
801
+ parentPathname,
802
+ !parentRoute || parentPath.endsWith('*') || parentPath.endsWith('*?'),
803
+ `You rendered descendant <Routes> (or called \`useRoutes()\`) at ` +
804
+ `"${parentPathname}" (under <Route path="${parentPath}">) but the ` +
805
+ `parent route path has no trailing "*". This means if you navigate ` +
806
+ `deeper, the parent won't match anymore and therefore the child ` +
807
+ `routes will never render.\n\n` +
808
+ `Please change the parent <Route path="${parentPath}"> to <Route ` +
809
+ `path="${parentPath === '/' ? '*' : `${parentPath}/*`}">.`,
810
+ );
811
+ }
812
+
813
+ let locationFromContext = useLocation();
814
+
815
+ let location;
816
+ if (locationArg) {
817
+ let parsedLocationArg = typeof locationArg === 'string' ? parsePath(locationArg) : locationArg;
818
+
819
+ invariant(
820
+ parentPathnameBase === '/' || parsedLocationArg.pathname?.startsWith(parentPathnameBase),
821
+ `When overriding the location using \`<Routes location>\` or \`useRoutes(routes, location)\`, ` +
822
+ `the location pathname must begin with the portion of the URL pathname that was ` +
823
+ `matched by all parent routes. The current pathname base is "${parentPathnameBase}" ` +
824
+ `but pathname "${parsedLocationArg.pathname}" was given in the \`location\` prop.`,
825
+ );
826
+
827
+ location = parsedLocationArg;
828
+ } else {
829
+ location = locationFromContext;
830
+ }
831
+
832
+ let pathname = location.pathname || '/';
833
+
834
+ let remainingPathname = pathname;
835
+ if (parentPathnameBase !== '/') {
836
+ // Determine the remaining pathname by removing the # of URL segments the
837
+ // parentPathnameBase has, instead of removing based on character count.
838
+ // This is because we can't guarantee that incoming/outgoing encodings/
839
+ // decodings will match exactly.
840
+ // We decode paths before matching on a per-segment basis with
841
+ // decodeURIComponent(), but we re-encode pathnames via `new URL()` so they
842
+ // match what `window.location.pathname` would reflect. Those don't 100%
843
+ // align when it comes to encoded URI characters such as % and &.
844
+ //
845
+ // So we may end up with:
846
+ // pathname: "/descendant/a%25b/match"
847
+ // parentPathnameBase: "/descendant/a%b"
848
+ //
849
+ // And the direct substring removal approach won't work :/
850
+ let parentSegments = parentPathnameBase.replace(/^\//, '').split('/');
851
+ let segments = pathname.replace(/^\//, '').split('/');
852
+ remainingPathname = '/' + segments.slice(parentSegments.length).join('/');
853
+ }
854
+
855
+ let matches =
856
+ dataRouterOpts && dataRouterOpts.state.matches.length
857
+ ? // If we're in a data router, use the matches we've already identified but ensure
858
+ // we have the latest route instances from the manifest in case elements have changed
859
+ dataRouterOpts.state.matches.map((m) =>
860
+ Object.assign(m, {
861
+ route: dataRouterOpts!.manifest[m.route.id] || m.route,
862
+ }),
863
+ )
864
+ : matchRoutes(routes, { pathname: remainingPathname });
865
+
866
+ if (ENABLE_DEV_WARNINGS) {
867
+ warning(
868
+ parentRoute || matches != null,
869
+ `No routes matched location "${location.pathname}${location.search}${location.hash}" `,
870
+ );
871
+
872
+ warning(
873
+ matches == null ||
874
+ matches[matches.length - 1].route.element !== undefined ||
875
+ matches[matches.length - 1].route.Component !== undefined ||
876
+ matches[matches.length - 1].route.lazy !== undefined,
877
+ `Matched leaf route at location "${location.pathname}${location.search}${location.hash}" ` +
878
+ `does not have an element or Component. This means it will render an <Outlet /> with a ` +
879
+ `null value by default resulting in an "empty" page.`,
880
+ );
881
+ }
882
+
883
+ let renderedMatches = _renderMatches(
884
+ matches &&
885
+ matches.map((match) =>
886
+ Object.assign({}, match, {
887
+ params: Object.assign({}, parentParams, match.params),
888
+ pathname: joinPaths([
889
+ parentPathnameBase,
890
+ // Re-encode pathnames that were decoded inside matchRoutes.
891
+ // Pre-encode `%`, `?` and `#` ahead of `encodeLocation` because it uses
892
+ // `new URL()` internally and we need to prevent it from treating
893
+ // them as separators
894
+ navigator.encodeLocation
895
+ ? navigator.encodeLocation(
896
+ match.pathname.replace(/%/g, '%25').replace(/\?/g, '%3F').replace(/#/g, '%23'),
897
+ ).pathname
898
+ : match.pathname,
899
+ ]),
900
+ pathnameBase:
901
+ match.pathnameBase === '/'
902
+ ? parentPathnameBase
903
+ : joinPaths([
904
+ parentPathnameBase,
905
+ // Re-encode pathnames that were decoded inside matchRoutes
906
+ // Pre-encode `%`, `?` and `#` ahead of `encodeLocation` because it uses
907
+ // `new URL()` internally and we need to prevent it from treating
908
+ // them as separators
909
+ navigator.encodeLocation
910
+ ? navigator.encodeLocation(
911
+ match.pathnameBase
912
+ .replace(/%/g, '%25')
913
+ .replace(/\?/g, '%3F')
914
+ .replace(/#/g, '%23'),
915
+ ).pathname
916
+ : match.pathnameBase,
917
+ ]),
918
+ }),
919
+ ),
920
+ parentMatches,
921
+ dataRouterOpts,
922
+ );
923
+
924
+ // When a user passes in a `locationArg`, the associated routes need to
925
+ // be wrapped in a new `LocationContext.Provider` in order for `useLocation`
926
+ // to use the scoped location instead of the global location.
927
+ if (locationArg && renderedMatches) {
928
+ return createElement(LocationContext.Provider as any, {
929
+ value: {
930
+ location: {
931
+ pathname: '/',
932
+ search: '',
933
+ hash: '',
934
+ state: null,
935
+ key: 'default',
936
+ mask: undefined,
937
+ ...location,
938
+ },
939
+ navigationType: NavigationType.Pop,
940
+ },
941
+ children: renderedMatches,
942
+ });
943
+ }
944
+
945
+ return renderedMatches;
946
+ }
947
+
948
+ // OCTANE: the inline `function DefaultErrorComponent()` lives in
949
+ // ./DefaultErrorComponent.tsrx (JSX needs a compiled file); imported above.
950
+ // Upstream: `const defaultErrorElement = <DefaultErrorComponent />;`
951
+ const defaultErrorElement = createElement(DefaultErrorComponent);
952
+
953
+ // OCTANE: the `class RenderErrorBoundary extends React.Component` lives in
954
+ // ./RenderErrorBoundary.tsrx (imported above, re-exported here to keep
955
+ // upstream's export surface). Its RSC digest decoding
956
+ // (decodeRouteErrorResponseDigest via the RSCRouterContext contextType) is
957
+ // dropped — no octane RSC runtime.
958
+ export { RenderErrorBoundary };
959
+
960
+ // OCTANE: dropped — `errorRedirectHandledMap` and the `RSCErrorHandler`
961
+ // component (RSC redirect-digest handling via decodeRedirectErrorDigest,
962
+ // parseToInfo, hasInvalidProtocol, isBrowser) have no octane RSC runtime.
963
+
964
+ interface RenderedRouteProps {
965
+ routeContext: RouteContextObject;
966
+ match: RouteMatch<string, RouteObject>;
967
+ children: unknown | null;
968
+ }
969
+
970
+ function RenderedRoute({ routeContext, match, children }: RenderedRouteProps) {
971
+ let dataRouterContext = useContext(DataRouterContext);
972
+
973
+ // Track how deep we got in our render pass to emulate SSR componentDidCatch
974
+ // in a DataStaticRouter
975
+ if (
976
+ dataRouterContext &&
977
+ dataRouterContext.static &&
978
+ dataRouterContext.staticContext &&
979
+ (match.route.errorElement || match.route.ErrorBoundary)
980
+ ) {
981
+ dataRouterContext.staticContext._deepestRenderedBoundaryId = match.route.id;
982
+ }
983
+
984
+ return createElement(RouteContext.Provider as any, { value: routeContext, children });
985
+ }
986
+
987
+ export function _renderMatches(
988
+ matches: RouteMatch[] | null,
989
+ parentMatches: RouteMatch[] = [],
990
+ dataRouterOpts?: {
991
+ state: DataRouter['state'];
992
+ isStatic: boolean;
993
+ onError: ClientOnErrorFunction | undefined;
994
+ future: DataRouter['future'];
995
+ },
996
+ ): unknown {
997
+ let dataRouterState = dataRouterOpts?.state;
998
+
999
+ if (matches == null) {
1000
+ if (!dataRouterState) {
1001
+ return null;
1002
+ }
1003
+
1004
+ if (dataRouterState.errors) {
1005
+ // Don't bail if we have data router errors so we can render them in the
1006
+ // boundary. Use the pre-matched (or shimmed) matches
1007
+ matches = dataRouterState.matches as DataRouteMatch[];
1008
+ } else if (
1009
+ parentMatches.length === 0 &&
1010
+ !dataRouterState.initialized &&
1011
+ dataRouterState.matches.length > 0
1012
+ ) {
1013
+ // Don't bail if we're initializing with partial hydration and we have
1014
+ // router matches. That means we're actively running `patchRoutesOnNavigation`
1015
+ // so we should render down the partial matches to the appropriate
1016
+ // `HydrateFallback`. We only do this if `parentMatches` is empty so it
1017
+ // only impacts the root matches for `RouterProvider` and no descendant
1018
+ // `<Routes>`
1019
+ matches = dataRouterState.matches as DataRouteMatch[];
1020
+ } else {
1021
+ return null;
1022
+ }
1023
+ }
1024
+
1025
+ let renderedMatches = matches;
1026
+
1027
+ // If we have data errors, trim matches to the highest error boundary
1028
+ let errors = dataRouterState?.errors;
1029
+ if (errors != null) {
1030
+ let errorIndex = renderedMatches.findIndex(
1031
+ (m) => m.route.id && errors?.[m.route.id] !== undefined,
1032
+ );
1033
+ invariant(
1034
+ errorIndex >= 0,
1035
+ `Could not find a matching route for errors on route IDs: ${Object.keys(errors).join(',')}`,
1036
+ );
1037
+ renderedMatches = renderedMatches.slice(0, Math.min(renderedMatches.length, errorIndex + 1));
1038
+ }
1039
+
1040
+ // If we're in a partial hydration mode, detect if we need to render down to
1041
+ // a given HydrateFallback while we load the rest of the hydration data
1042
+ let renderFallback = false;
1043
+ let fallbackIndex = -1;
1044
+ if (dataRouterOpts && dataRouterState) {
1045
+ renderFallback = dataRouterState.renderFallback;
1046
+ for (let i = 0; i < renderedMatches.length; i++) {
1047
+ let match = renderedMatches[i];
1048
+ // Track the deepest fallback up until the first route without data
1049
+ if (match.route.HydrateFallback || match.route.hydrateFallbackElement) {
1050
+ fallbackIndex = i;
1051
+ }
1052
+
1053
+ if (match.route.id) {
1054
+ let { loaderData, errors } = dataRouterState;
1055
+ let needsToRunLoader =
1056
+ match.route.loader &&
1057
+ !loaderData.hasOwnProperty(match.route.id) &&
1058
+ (!errors || errors[match.route.id] === undefined);
1059
+ if (match.route.lazy || needsToRunLoader) {
1060
+ // We found the first route that's not ready to render (waiting on
1061
+ // lazy, or has a loader that hasn't run yet) - render up until the
1062
+ // appropriate fallback
1063
+ if (dataRouterOpts.isStatic) {
1064
+ renderFallback = true;
1065
+ }
1066
+ if (fallbackIndex >= 0) {
1067
+ renderedMatches = renderedMatches.slice(0, fallbackIndex + 1);
1068
+ } else {
1069
+ renderedMatches = [renderedMatches[0]];
1070
+ }
1071
+ break;
1072
+ }
1073
+ }
1074
+ }
1075
+ }
1076
+
1077
+ let onErrorHandler = dataRouterOpts?.onError;
1078
+ let onError =
1079
+ dataRouterState && onErrorHandler
1080
+ ? (error: unknown, errorInfo?: unknown) => {
1081
+ onErrorHandler(error, {
1082
+ location: dataRouterState.location,
1083
+ params: dataRouterState.matches?.[0]?.params ?? {},
1084
+ pattern: getRoutePattern(dataRouterState.matches),
1085
+ errorInfo,
1086
+ });
1087
+ }
1088
+ : undefined;
1089
+
1090
+ return renderedMatches.reduceRight((outlet, match, index) => {
1091
+ // Only data routers handle errors/fallbacks
1092
+ let error: any;
1093
+ let shouldRenderHydrateFallback = false;
1094
+ let errorElement: unknown | null = null;
1095
+ let hydrateFallbackElement: unknown | null = null;
1096
+ if (dataRouterState) {
1097
+ error = errors && match.route.id ? errors[match.route.id] : undefined;
1098
+ errorElement = match.route.errorElement || defaultErrorElement;
1099
+
1100
+ if (renderFallback) {
1101
+ if (fallbackIndex < 0 && index === 0) {
1102
+ warningOnce(
1103
+ 'route-fallback',
1104
+ false,
1105
+ 'No `HydrateFallback` element provided to render during initial hydration',
1106
+ );
1107
+ shouldRenderHydrateFallback = true;
1108
+ hydrateFallbackElement = null;
1109
+ } else if (fallbackIndex === index) {
1110
+ shouldRenderHydrateFallback = true;
1111
+ hydrateFallbackElement = match.route.hydrateFallbackElement || null;
1112
+ }
1113
+ }
1114
+ }
1115
+
1116
+ let matches = parentMatches.concat(renderedMatches.slice(0, index + 1));
1117
+ let getChildren = () => {
1118
+ let children: unknown;
1119
+ if (error) {
1120
+ children = errorElement;
1121
+ } else if (shouldRenderHydrateFallback) {
1122
+ children = hydrateFallbackElement;
1123
+ } else if (match.route.Component) {
1124
+ // Note: This is a de-optimized path since React won't re-use the
1125
+ // ReactElement since it's identity changes with each new
1126
+ // React.createElement call. We keep this so folks can use
1127
+ // `<Route Component={...}>` in `<Routes>` but generally `Component`
1128
+ // usage is only advised in `RouterProvider` when we can convert it to
1129
+ // `element` ahead of time.
1130
+ children = createElement(match.route.Component as any);
1131
+ } else if (match.route.element) {
1132
+ children = match.route.element;
1133
+ } else {
1134
+ children = outlet;
1135
+ }
1136
+
1137
+ return createElement(RenderedRoute as any, {
1138
+ match,
1139
+ routeContext: {
1140
+ outlet,
1141
+ matches,
1142
+ isDataRoute: dataRouterState != null,
1143
+ },
1144
+ children,
1145
+ });
1146
+ };
1147
+ // Only wrap in an error boundary within data router usages when we have an
1148
+ // ErrorBoundary/errorElement on this route. Otherwise let it bubble up to
1149
+ // an ancestor ErrorBoundary/errorElement
1150
+ return dataRouterState && (match.route.ErrorBoundary || match.route.errorElement || index === 0)
1151
+ ? createElement(RenderErrorBoundary as any, {
1152
+ location: dataRouterState.location,
1153
+ revalidation: dataRouterState.revalidation,
1154
+ component: errorElement,
1155
+ error,
1156
+ children: getChildren(),
1157
+ routeContext: { outlet: null, matches, isDataRoute: true },
1158
+ onError,
1159
+ })
1160
+ : getChildren();
1161
+ }, null as unknown);
1162
+ }
1163
+
1164
+ enum DataRouterHook {
1165
+ UseBlocker = 'useBlocker',
1166
+ UseRevalidator = 'useRevalidator',
1167
+ UseNavigateStable = 'useNavigate',
1168
+ }
1169
+
1170
+ enum DataRouterStateHook {
1171
+ UseBlocker = 'useBlocker',
1172
+ UseLoaderData = 'useLoaderData',
1173
+ UseActionData = 'useActionData',
1174
+ UseRouteError = 'useRouteError',
1175
+ UseNavigation = 'useNavigation',
1176
+ UseRouteLoaderData = 'useRouteLoaderData',
1177
+ UseMatches = 'useMatches',
1178
+ UseRevalidator = 'useRevalidator',
1179
+ UseNavigateStable = 'useNavigate',
1180
+ UseRouteId = 'useRouteId',
1181
+ UseRoute = 'useRoute',
1182
+ UseRouterState = 'unstable_useRouterState',
1183
+ }
1184
+
1185
+ function getDataRouterConsoleError(hookName: DataRouterHook | DataRouterStateHook) {
1186
+ return `${hookName} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`;
1187
+ }
1188
+
1189
+ function useDataRouterContext(hookName: DataRouterHook) {
1190
+ let ctx = useContext(DataRouterContext);
1191
+ invariant(ctx, getDataRouterConsoleError(hookName));
1192
+ return ctx;
1193
+ }
1194
+
1195
+ function useDataRouterState(hookName: DataRouterStateHook) {
1196
+ let state = useContext(DataRouterStateContext);
1197
+ invariant(state, getDataRouterConsoleError(hookName));
1198
+ return state;
1199
+ }
1200
+
1201
+ function useRouteContext(hookName: DataRouterStateHook) {
1202
+ let route = useContext(RouteContext);
1203
+ invariant(route, getDataRouterConsoleError(hookName));
1204
+ return route;
1205
+ }
1206
+
1207
+ // Internal version with hookName-aware debugging
1208
+ function useCurrentRouteId(hookName: DataRouterStateHook) {
1209
+ let route = useRouteContext(hookName);
1210
+ let thisRoute = route.matches[route.matches.length - 1];
1211
+ invariant(
1212
+ thisRoute.route.id,
1213
+ `${hookName} can only be used on routes that contain a unique "id"`,
1214
+ );
1215
+ return thisRoute.route.id;
1216
+ }
1217
+
1218
+ /**
1219
+ * Returns the ID for the nearest contextual route
1220
+ *
1221
+ * @category Hooks
1222
+ * @returns The ID of the nearest contextual route
1223
+ */
1224
+ export function useRouteId() {
1225
+ return useCurrentRouteId(DataRouterStateHook.UseRouteId);
1226
+ }
1227
+
1228
+ // Omit the fields from each navigation state individually to preserve the discriminated union
1229
+ type UseNavigationResult = UseNavigationResultStates[keyof UseNavigationResultStates];
1230
+
1231
+ type UseNavigationResultStates = {
1232
+ Idle: Omit<NavigationStates['Idle'], 'matches' | 'historyAction'>;
1233
+ Loading: Omit<NavigationStates['Loading'], 'matches' | 'historyAction'>;
1234
+ Submitting: Omit<NavigationStates['Submitting'], 'matches' | 'historyAction'>;
1235
+ };
1236
+
1237
+ /**
1238
+ * Returns the current {@link Navigation}, defaulting to an "idle" navigation
1239
+ * when no navigation is in progress. You can use this to render pending UI
1240
+ * (like a global spinner) or read [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
1241
+ * from a form navigation.
1242
+ *
1243
+ * @example
1244
+ * import { useNavigation } from "react-router";
1245
+ *
1246
+ * function SomeComponent() {
1247
+ * let navigation = useNavigation();
1248
+ * navigation.state;
1249
+ * navigation.formData;
1250
+ * // etc.
1251
+ * }
1252
+ *
1253
+ * @public
1254
+ * @category Hooks
1255
+ * @mode framework
1256
+ * @mode data
1257
+ * @returns The current {@link Navigation} object
1258
+ */
1259
+ export function useNavigation(...args: any[]): UseNavigationResult {
1260
+ const [, slot] = splitSlot(args);
1261
+ let state = useDataRouterState(DataRouterStateHook.UseNavigation);
1262
+ return useMemo<UseNavigationResult>(
1263
+ () => {
1264
+ let { matches, historyAction, ...rest } = state.navigation;
1265
+ return rest;
1266
+ },
1267
+ [state.navigation],
1268
+ subSlot(slot, 'nav:memo'),
1269
+ );
1270
+ }
1271
+
1272
+ /**
1273
+ * Revalidate the data on the page for reasons outside of normal data mutations
1274
+ * like [`Window` focus](https://developer.mozilla.org/en-US/docs/Web/API/Window/focus_event)
1275
+ * or polling on an interval.
1276
+ *
1277
+ * Note that page data is already revalidated automatically after actions.
1278
+ * If you find yourself using this for normal CRUD operations on your data in
1279
+ * response to user interactions, you're probably not taking advantage of the
1280
+ * other APIs like {@link useFetcher}, {@link Form}, {@link useSubmit} that do
1281
+ * this automatically.
1282
+ *
1283
+ * @example
1284
+ * import { useRevalidator } from "react-router";
1285
+ *
1286
+ * function WindowFocusRevalidator() {
1287
+ * const revalidator = useRevalidator();
1288
+ *
1289
+ * useFakeWindowFocus(() => {
1290
+ * revalidator.revalidate();
1291
+ * });
1292
+ *
1293
+ * return (
1294
+ * <div hidden={revalidator.state === "idle"}>
1295
+ * Revalidating...
1296
+ * </div>
1297
+ * );
1298
+ * }
1299
+ *
1300
+ * @public
1301
+ * @category Hooks
1302
+ * @mode framework
1303
+ * @mode data
1304
+ * @returns An object with a `revalidate` function and the current revalidation
1305
+ * `state`
1306
+ */
1307
+ export function useRevalidator(...args: any[]): {
1308
+ revalidate: () => Promise<void>;
1309
+ state: DataRouter['state']['revalidation'];
1310
+ } {
1311
+ const [, slot] = splitSlot(args);
1312
+ let dataRouterContext = useDataRouterContext(DataRouterHook.UseRevalidator);
1313
+ let state = useDataRouterState(DataRouterStateHook.UseRevalidator);
1314
+ let revalidate = useCallback(
1315
+ async () => {
1316
+ await dataRouterContext.router.revalidate();
1317
+ },
1318
+ [dataRouterContext.router],
1319
+ subSlot(slot, 'reval:cb'),
1320
+ );
1321
+
1322
+ return useMemo(
1323
+ () => ({ revalidate, state: state.revalidation }),
1324
+ [revalidate, state.revalidation],
1325
+ subSlot(slot, 'reval:memo'),
1326
+ );
1327
+ }
1328
+
1329
+ /**
1330
+ * Returns the active route matches, useful for accessing `loaderData` for
1331
+ * parent/child routes or the route [`handle`](../../start/framework/route-module#handle)
1332
+ * property
1333
+ *
1334
+ * @public
1335
+ * @category Hooks
1336
+ * @mode framework
1337
+ * @mode data
1338
+ * @returns An array of {@link UIMatch | UI matches} for the current route hierarchy
1339
+ */
1340
+ export function useMatches(...args: any[]): UIMatch[] {
1341
+ const [, slot] = splitSlot(args);
1342
+ let { matches, loaderData } = useDataRouterState(DataRouterStateHook.UseMatches);
1343
+ return useMemo(
1344
+ () => matches.map((m) => convertRouteMatchToUiMatch(m, loaderData)),
1345
+ [matches, loaderData],
1346
+ subSlot(slot, 'matches:memo'),
1347
+ );
1348
+ }
1349
+
1350
+ // OCTANE: local substitute — upstream's `SerializeFrom` comes from
1351
+ // ./types/route-data (framework-mode serialization typing, not vendored).
1352
+ // Loose equivalent keeps the public hook generics shaped like upstream
1353
+ // (tanstack-router precedent: loose public generics).
1354
+ type SerializeFrom<T> = T extends (...args: any[]) => infer U ? Awaited<U> : Awaited<T>;
1355
+
1356
+ /**
1357
+ * Returns the data from the closest route
1358
+ * [`loader`](../../start/framework/route-module#loader) or
1359
+ * [`clientLoader`](../../start/framework/route-module#clientloader).
1360
+ *
1361
+ * @example
1362
+ * import { useLoaderData } from "react-router";
1363
+ *
1364
+ * export async function loader() {
1365
+ * return await fakeDb.invoices.findAll();
1366
+ * }
1367
+ *
1368
+ * export default function Invoices() {
1369
+ * let invoices = useLoaderData<typeof loader>();
1370
+ * // ...
1371
+ * }
1372
+ *
1373
+ * @public
1374
+ * @category Hooks
1375
+ * @mode framework
1376
+ * @mode data
1377
+ * @returns The data returned from the route's [`loader`](../../start/framework/route-module#loader) or [`clientLoader`](../../start/framework/route-module#clientloader) function
1378
+ */
1379
+ export function useLoaderData<T = any>(): SerializeFrom<T> {
1380
+ let state = useDataRouterState(DataRouterStateHook.UseLoaderData);
1381
+ let routeId = useCurrentRouteId(DataRouterStateHook.UseLoaderData);
1382
+ return state.loaderData[routeId] as SerializeFrom<T>;
1383
+ }
1384
+
1385
+ /**
1386
+ * Returns the [`loader`](../../start/framework/route-module#loader) data for a
1387
+ * given route by route ID.
1388
+ *
1389
+ * Route IDs are created automatically. They are simply the path of the route file
1390
+ * relative to the app folder without the extension.
1391
+ *
1392
+ * | Route Filename | Route ID |
1393
+ * | ---------------------------- | ---------------------- |
1394
+ * | `app/root.tsx` | `"root"` |
1395
+ * | `app/routes/teams.tsx` | `"routes/teams"` |
1396
+ * | `app/whatever/teams.$id.tsx` | `"whatever/teams.$id"` |
1397
+ *
1398
+ * @example
1399
+ * import { useRouteLoaderData } from "react-router";
1400
+ *
1401
+ * function SomeComponent() {
1402
+ * const { user } = useRouteLoaderData("root");
1403
+ * }
1404
+ *
1405
+ * // You can also specify your own route ID's manually in your routes.ts file:
1406
+ * route("/", "containers/app.tsx", { id: "app" })
1407
+ * useRouteLoaderData("app");
1408
+ *
1409
+ * @public
1410
+ * @category Hooks
1411
+ * @mode framework
1412
+ * @mode data
1413
+ * @param routeId The ID of the route to return loader data from
1414
+ * @returns The data returned from the specified route's [`loader`](../../start/framework/route-module#loader)
1415
+ * function, or `undefined` if not found
1416
+ */
1417
+ export function useRouteLoaderData<T = any>(routeId: string): SerializeFrom<T> | undefined {
1418
+ let state = useDataRouterState(DataRouterStateHook.UseRouteLoaderData);
1419
+ return state.loaderData[routeId] as SerializeFrom<T> | undefined;
1420
+ }
1421
+
1422
+ /**
1423
+ * Returns the [`action`](../../start/framework/route-module#action) data from
1424
+ * the most recent `POST` navigation form submission or `undefined` if there
1425
+ * hasn't been one.
1426
+ *
1427
+ * @example
1428
+ * import { Form, useActionData } from "react-router";
1429
+ *
1430
+ * export async function action({ request }) {
1431
+ * const body = await request.formData();
1432
+ * const name = body.get("visitorsName");
1433
+ * return { message: `Hello, ${name}` };
1434
+ * }
1435
+ *
1436
+ * export default function Invoices() {
1437
+ * const data = useActionData();
1438
+ * return (
1439
+ * <Form method="post">
1440
+ * <input type="text" name="visitorsName" />
1441
+ * {data ? data.message : "Waiting..."}
1442
+ * </Form>
1443
+ * );
1444
+ * }
1445
+ *
1446
+ * @public
1447
+ * @category Hooks
1448
+ * @mode framework
1449
+ * @mode data
1450
+ * @returns The data returned from the route's [`action`](../../start/framework/route-module#action)
1451
+ * function, or `undefined` if no [`action`](../../start/framework/route-module#action)
1452
+ * has been called
1453
+ */
1454
+ export function useActionData<T = any>(): SerializeFrom<T> | undefined {
1455
+ let state = useDataRouterState(DataRouterStateHook.UseActionData);
1456
+ let routeId = useCurrentRouteId(DataRouterStateHook.UseLoaderData);
1457
+ return (state.actionData ? state.actionData[routeId] : undefined) as SerializeFrom<T> | undefined;
1458
+ }
1459
+
1460
+ /**
1461
+ * Accesses the error thrown during an
1462
+ * [`action`](../../start/framework/route-module#action),
1463
+ * [`loader`](../../start/framework/route-module#loader),
1464
+ * or component render to be used in a route module
1465
+ * [`ErrorBoundary`](../../start/framework/route-module#errorboundary).
1466
+ *
1467
+ * @example
1468
+ * export function ErrorBoundary() {
1469
+ * const error = useRouteError();
1470
+ * return <div>{error.message}</div>;
1471
+ * }
1472
+ *
1473
+ * @public
1474
+ * @category Hooks
1475
+ * @mode framework
1476
+ * @mode data
1477
+ * @returns The error that was thrown during route [loading](../../start/framework/route-module#loader),
1478
+ * [`action`](../../start/framework/route-module#action) execution, or rendering
1479
+ */
1480
+ export function useRouteError(): unknown {
1481
+ let error = useContext(RouteErrorContext);
1482
+ let state = useDataRouterState(DataRouterStateHook.UseRouteError);
1483
+ let routeId = useCurrentRouteId(DataRouterStateHook.UseRouteError);
1484
+
1485
+ // If this was a render error, we put it in a RouteError context inside
1486
+ // of RenderErrorBoundary
1487
+ if (error !== undefined) {
1488
+ return error;
1489
+ }
1490
+
1491
+ // Otherwise look for errors from our data router state
1492
+ return state.errors?.[routeId];
1493
+ }
1494
+
1495
+ /**
1496
+ * Returns the resolved promise value from the closest {@link Await | `<Await>`}.
1497
+ *
1498
+ * @example
1499
+ * function SomeDescendant() {
1500
+ * const value = useAsyncValue();
1501
+ * // ...
1502
+ * }
1503
+ *
1504
+ * // somewhere in your app
1505
+ * <Await resolve={somePromise}>
1506
+ * <SomeDescendant />
1507
+ * </Await>;
1508
+ *
1509
+ * @public
1510
+ * @category Hooks
1511
+ * @mode framework
1512
+ * @mode data
1513
+ * @returns The resolved value from the nearest {@link Await} component
1514
+ */
1515
+ export function useAsyncValue(): unknown {
1516
+ let value = useContext(AwaitContext);
1517
+ return value?._data;
1518
+ }
1519
+
1520
+ /**
1521
+ * Returns the rejection value from the closest {@link Await | `<Await>`}.
1522
+ *
1523
+ * @example
1524
+ * import { Await, useAsyncError } from "react-router";
1525
+ *
1526
+ * function ErrorElement() {
1527
+ * const error = useAsyncError();
1528
+ * return (
1529
+ * <p>Uh Oh, something went wrong! {error.message}</p>
1530
+ * );
1531
+ * }
1532
+ *
1533
+ * // somewhere in your app
1534
+ * <Await
1535
+ * resolve={promiseThatRejects}
1536
+ * errorElement={<ErrorElement />}
1537
+ * />;
1538
+ *
1539
+ * @public
1540
+ * @category Hooks
1541
+ * @mode framework
1542
+ * @mode data
1543
+ * @returns The error that was thrown in the nearest {@link Await} component
1544
+ */
1545
+ export function useAsyncError(): unknown {
1546
+ let value = useContext(AwaitContext);
1547
+ return value?._error;
1548
+ }
1549
+
1550
+ let blockerId = 0;
1551
+
1552
+ /**
1553
+ * Allow the application to block navigations within the SPA and present the
1554
+ * user a confirmation dialog to confirm the navigation.
1555
+ */
1556
+ export function useBlocker(shouldBlock: boolean | BlockerFunction, ...args: any[]): Blocker {
1557
+ const [, slot] = splitSlot(args);
1558
+ let { router, basename } = useDataRouterContext(DataRouterHook.UseBlocker);
1559
+ let state = useDataRouterState(DataRouterStateHook.UseBlocker);
1560
+
1561
+ let [blockerKey, setBlockerKey] = useState('', subSlot(slot, 'ub:key'));
1562
+ let blockerFunction = useCallback(
1563
+ ((arg: Parameters<BlockerFunction>[0]) => {
1564
+ if (typeof shouldBlock !== 'function') {
1565
+ return !!shouldBlock;
1566
+ }
1567
+ if (basename === '/') {
1568
+ return shouldBlock(arg);
1569
+ }
1570
+
1571
+ // If they provided us a function and we've got an active basename, strip
1572
+ // it from the locations we expose to the user to match the behavior of
1573
+ // useLocation
1574
+ let { currentLocation, nextLocation, historyAction } = arg;
1575
+ return shouldBlock({
1576
+ currentLocation: {
1577
+ ...currentLocation,
1578
+ pathname: stripBasename(currentLocation.pathname, basename) || currentLocation.pathname,
1579
+ },
1580
+ nextLocation: {
1581
+ ...nextLocation,
1582
+ pathname: stripBasename(nextLocation.pathname, basename) || nextLocation.pathname,
1583
+ },
1584
+ historyAction,
1585
+ });
1586
+ }) as BlockerFunction,
1587
+ [basename, shouldBlock],
1588
+ subSlot(slot, 'ub:fn'),
1589
+ );
1590
+
1591
+ // This effect is in charge of blocker key assignment and deletion (which is
1592
+ // tightly coupled to the key)
1593
+ useEffect(
1594
+ () => {
1595
+ let key = String(++blockerId);
1596
+ setBlockerKey(key);
1597
+ return () => router.deleteBlocker(key);
1598
+ },
1599
+ [router],
1600
+ subSlot(slot, 'ub:keyEff'),
1601
+ );
1602
+
1603
+ // This effect handles assigning the blockerFunction. This is to handle
1604
+ // unstable blocker function identities, and happens only after the prior
1605
+ // effect so we don't get an orphaned blockerFunction in the router with a
1606
+ // key of "". Until then we just have the IDLE_BLOCKER.
1607
+ useEffect(
1608
+ () => {
1609
+ if (blockerKey !== '') {
1610
+ router.getBlocker(blockerKey, blockerFunction);
1611
+ }
1612
+ },
1613
+ [router, blockerKey, blockerFunction],
1614
+ subSlot(slot, 'ub:fnEff'),
1615
+ );
1616
+
1617
+ // Prefer the blocker from `state` not `router.state` since DataRouterContext
1618
+ // is memoized so this ensures we update on blocker state updates
1619
+ return blockerKey && state.blockers.has(blockerKey)
1620
+ ? state.blockers.get(blockerKey)!
1621
+ : IDLE_BLOCKER;
1622
+ }
1623
+
1624
+ // Stable version of useNavigate that is used when we are in the context of
1625
+ // a RouterProvider.
1626
+ function useNavigateStable(slot?: symbol): NavigateFunction {
1627
+ let { router } = useDataRouterContext(DataRouterHook.UseNavigateStable);
1628
+ let id = useCurrentRouteId(DataRouterStateHook.UseNavigateStable);
1629
+
1630
+ let activeRef = useRef(false, subSlot(slot, 'snav:ref'));
1631
+ useIsomorphicLayoutEffect(
1632
+ () => {
1633
+ activeRef.current = true;
1634
+ },
1635
+ subSlot(slot, 'snav:ile'),
1636
+ );
1637
+
1638
+ let navigate: NavigateFunction = useCallback(
1639
+ async (to: To | number, options: NavigateOptions = {}) => {
1640
+ warning(activeRef.current, navigateEffectWarning);
1641
+
1642
+ // Short circuit here since if this happens on first render the navigate
1643
+ // is useless because we haven't wired up our router subscriber yet
1644
+ if (!activeRef.current) return;
1645
+
1646
+ if (typeof to === 'number') {
1647
+ await router.navigate(to);
1648
+ } else {
1649
+ await router.navigate(to, { fromRouteId: id, ...options });
1650
+ }
1651
+ },
1652
+ [router, id],
1653
+ subSlot(slot, 'snav:cb'),
1654
+ );
1655
+
1656
+ return navigate;
1657
+ }
1658
+
1659
+ const alreadyWarned: Record<string, boolean> = {};
1660
+
1661
+ function warningOnce(key: string, cond: boolean, message: string) {
1662
+ if (!cond && !alreadyWarned[key]) {
1663
+ alreadyWarned[key] = true;
1664
+ warning(false, message);
1665
+ }
1666
+ }
1667
+
1668
+ // OCTANE: upstream types useRoute over ./types/register's RouteModules and
1669
+ // ./types/route-data's GetLoaderData/GetActionData (framework-mode
1670
+ // serialization typing, not vendored) — declared loosely here.
1671
+ export function useRoute(...args: any[]): any {
1672
+ const [user] = splitSlot(args);
1673
+ const currentRouteId = useCurrentRouteId(DataRouterStateHook.UseRoute);
1674
+ const id: string = user[0] ?? currentRouteId;
1675
+
1676
+ const state = useDataRouterState(DataRouterStateHook.UseRoute);
1677
+ const route = state.matches.find(({ route }) => route.id === id);
1678
+
1679
+ if (route === undefined) return undefined;
1680
+ return {
1681
+ handle: route.route.handle,
1682
+ loaderData: state.loaderData[id],
1683
+ actionData: state.actionData?.[id],
1684
+ };
1685
+ }
1686
+
1687
+ /**
1688
+ * A single route match returned from `unstable_useRouterState`. Mirrors
1689
+ * UIMatch minus the data-related fields (`data`, `loaderData`).
1690
+ */
1691
+ export type unstable_RouterStateMatch<Handle = unknown> = Omit<
1692
+ UIMatch<unknown, Handle>,
1693
+ 'data' | 'loaderData'
1694
+ >;
1695
+
1696
+ export type unstable_RouterStateActiveVariant = {
1697
+ location: Location;
1698
+ searchParams: URLSearchParams;
1699
+ params: Params;
1700
+ matches: unstable_RouterStateMatch[];
1701
+ type: NavigationType;
1702
+ };
1703
+
1704
+ export type unstable_RouterStatePendingVariant = unstable_RouterStateActiveVariant & {
1705
+ state: 'loading' | 'submitting';
1706
+ formMethod: string | undefined;
1707
+ formAction: string | undefined;
1708
+ formEncType: string | undefined;
1709
+ formData: FormData | undefined;
1710
+ json: unknown;
1711
+ text: string | undefined;
1712
+ };
1713
+
1714
+ export type unstable_RouterState = {
1715
+ active: unstable_RouterStateActiveVariant;
1716
+ pending: unstable_RouterStatePendingVariant | null;
1717
+ };
1718
+
1719
+ function toRouterStateMatch(match: DataRouteMatch): unstable_RouterStateMatch {
1720
+ return {
1721
+ id: match.route.id,
1722
+ pathname: match.pathname,
1723
+ params: match.params,
1724
+ handle: match.route.handle,
1725
+ };
1726
+ }
1727
+
1728
+ /**
1729
+ * A unified hook for reading router state: current (`active`) and in-flight
1730
+ * (`pending`) locations, search params, params, matches, and navigation type.
1731
+ */
1732
+ export function useRouterState(...args: any[]): unstable_RouterState {
1733
+ const [, slot] = splitSlot(args);
1734
+ let {
1735
+ location,
1736
+ historyAction: type,
1737
+ matches,
1738
+ navigation,
1739
+ } = useDataRouterState(DataRouterStateHook.UseRouterState);
1740
+
1741
+ let active = useMemo(
1742
+ () => ({
1743
+ type,
1744
+ location,
1745
+ searchParams: new URLSearchParams(location.search),
1746
+ params: matches[matches.length - 1]?.params ?? {},
1747
+ matches: matches.map((m) => toRouterStateMatch(m)),
1748
+ }),
1749
+ [location, matches, type],
1750
+ subSlot(slot, 'urs:active'),
1751
+ ) as unstable_RouterStateActiveVariant;
1752
+
1753
+ let pending = useMemo(
1754
+ () => {
1755
+ if (navigation.state === 'idle') return null;
1756
+ let shared = {
1757
+ type: navigation.historyAction,
1758
+ location: navigation.location,
1759
+ searchParams: new URLSearchParams(navigation.location.search),
1760
+ params: navigation.matches[navigation.matches.length - 1]?.params ?? {},
1761
+ matches: navigation.matches.map((m: DataRouteMatch) => toRouterStateMatch(m)),
1762
+ };
1763
+
1764
+ // Do submissions fields independently to keep TS happy with the
1765
+ // `NavigationStates` discriminated union
1766
+ return navigation.state === 'loading'
1767
+ ? {
1768
+ ...shared,
1769
+ state: 'loading',
1770
+ formMethod: navigation.formMethod,
1771
+ formAction: navigation.formAction,
1772
+ formEncType: navigation.formEncType,
1773
+ formData: navigation.formData,
1774
+ json: navigation.json,
1775
+ text: navigation.text,
1776
+ }
1777
+ : {
1778
+ ...shared,
1779
+ state: 'submitting',
1780
+ formMethod: navigation.formMethod,
1781
+ formAction: navigation.formAction,
1782
+ formEncType: navigation.formEncType,
1783
+ formData: navigation.formData,
1784
+ json: navigation.json,
1785
+ text: navigation.text,
1786
+ };
1787
+ },
1788
+ [navigation],
1789
+ subSlot(slot, 'urs:pending'),
1790
+ ) as unstable_RouterStatePendingVariant | null;
1791
+
1792
+ return useMemo(
1793
+ () => ({ active, pending }),
1794
+ [active, pending],
1795
+ subSlot(slot, 'urs:combined'),
1796
+ ) as unstable_RouterState;
1797
+ }