react-router 6.3.0 → 6.4.0-pre.3

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 (71) hide show
  1. package/.eslintrc +12 -0
  2. package/CHANGELOG.md +8 -0
  3. package/__tests__/.eslintrc +8 -0
  4. package/__tests__/DataMemoryRouter-test.tsx +1902 -0
  5. package/__tests__/Route-test.tsx +45 -0
  6. package/__tests__/Router-basename-test.tsx +110 -0
  7. package/__tests__/Router-test.tsx +62 -0
  8. package/__tests__/Routes-location-test.tsx +69 -0
  9. package/__tests__/Routes-test.tsx +148 -0
  10. package/__tests__/__snapshots__/route-matching-test.tsx.snap +197 -0
  11. package/__tests__/absolute-path-matching-test.tsx +61 -0
  12. package/__tests__/createRoutesFromChildren-test.tsx +189 -0
  13. package/__tests__/descendant-routes-params-test.tsx +67 -0
  14. package/__tests__/descendant-routes-splat-matching-test.tsx +241 -0
  15. package/__tests__/descendant-routes-warning-test.tsx +140 -0
  16. package/__tests__/generatePath-test.tsx +45 -0
  17. package/__tests__/gh-issue-8127-test.tsx +32 -0
  18. package/__tests__/gh-issue-8165-test.tsx +97 -0
  19. package/__tests__/greedy-matching-test.tsx +89 -0
  20. package/__tests__/index-routes-test.tsx +24 -0
  21. package/__tests__/layout-routes-test.tsx +283 -0
  22. package/__tests__/matchPath-test.tsx +335 -0
  23. package/__tests__/matchRoutes-test.tsx +144 -0
  24. package/__tests__/navigate-test.tsx +49 -0
  25. package/__tests__/params-decode-test.tsx +36 -0
  26. package/__tests__/path-matching-test.tsx +270 -0
  27. package/__tests__/resolvePath-test.tsx +50 -0
  28. package/__tests__/route-depth-order-matching-test.tsx +135 -0
  29. package/__tests__/route-matching-test.tsx +164 -0
  30. package/__tests__/same-component-lifecycle-test.tsx +57 -0
  31. package/__tests__/setup.ts +15 -0
  32. package/__tests__/useHref-basename-test.tsx +351 -0
  33. package/__tests__/useHref-test.tsx +287 -0
  34. package/__tests__/useLocation-test.tsx +29 -0
  35. package/__tests__/useMatch-test.tsx +137 -0
  36. package/__tests__/useNavigate-test.tsx +100 -0
  37. package/__tests__/useOutlet-test.tsx +355 -0
  38. package/__tests__/useParams-test.tsx +212 -0
  39. package/__tests__/useResolvedPath-test.tsx +109 -0
  40. package/__tests__/useRoutes-test.tsx +122 -0
  41. package/__tests__/utils/renderStrict.tsx +21 -0
  42. package/__tests__/utils/waitForRedirect.tsx +5 -0
  43. package/index.ts +187 -0
  44. package/jest-transformer.js +10 -0
  45. package/jest.config.js +10 -0
  46. package/lib/components.tsx +491 -0
  47. package/lib/context.ts +96 -0
  48. package/lib/hooks.tsx +689 -0
  49. package/lib/use-sync-external-store-shim/index.ts +31 -0
  50. package/lib/use-sync-external-store-shim/useSyncExternalStoreShimClient.ts +153 -0
  51. package/lib/use-sync-external-store-shim/useSyncExternalStoreShimServer.ts +20 -0
  52. package/node-main.js +7 -0
  53. package/package.json +7 -4
  54. package/tsconfig.json +20 -0
  55. package/LICENSE.md +0 -22
  56. package/index.d.ts +0 -14
  57. package/index.js +0 -941
  58. package/index.js.map +0 -1
  59. package/lib/components.d.ts +0 -110
  60. package/lib/context.d.ts +0 -31
  61. package/lib/hooks.d.ts +0 -99
  62. package/lib/router.d.ts +0 -120
  63. package/main.js +0 -19
  64. package/react-router.development.js +0 -895
  65. package/react-router.development.js.map +0 -1
  66. package/react-router.production.min.js +0 -12
  67. package/react-router.production.min.js.map +0 -1
  68. package/umd/react-router.development.js +0 -990
  69. package/umd/react-router.development.js.map +0 -1
  70. package/umd/react-router.production.min.js +0 -12
  71. package/umd/react-router.production.min.js.map +0 -1
package/lib/hooks.tsx ADDED
@@ -0,0 +1,689 @@
1
+ import * as React from "react";
2
+ import {
3
+ isRouteErrorResponse,
4
+ Location,
5
+ ParamParseKey,
6
+ Params,
7
+ Path,
8
+ PathMatch,
9
+ PathPattern,
10
+ RouteMatch,
11
+ RouteObject,
12
+ Router as DataRouter,
13
+ To,
14
+ } from "@remix-run/router";
15
+ import {
16
+ Action as NavigationType,
17
+ getToPathname,
18
+ invariant,
19
+ joinPaths,
20
+ matchPath,
21
+ matchRoutes,
22
+ parsePath,
23
+ resolveTo,
24
+ warning,
25
+ } from "@remix-run/router";
26
+
27
+ import {
28
+ DataRouterContext,
29
+ DataRouterStateContext,
30
+ LocationContext,
31
+ NavigationContext,
32
+ NavigateOptions,
33
+ RouteContext,
34
+ RouteErrorContext,
35
+ } from "./context";
36
+
37
+ /**
38
+ * Returns the full href for the given "to" value. This is useful for building
39
+ * custom links that are also accessible and preserve right-click behavior.
40
+ *
41
+ * @see https://reactrouter.com/docs/en/v6/hooks/use-href
42
+ */
43
+ export function useHref(to: To): string {
44
+ invariant(
45
+ useInRouterContext(),
46
+ // TODO: This error is probably because they somehow have 2 versions of the
47
+ // router loaded. We can help them understand how to avoid that.
48
+ `useHref() may be used only in the context of a <Router> component.`
49
+ );
50
+
51
+ let { basename, navigator } = React.useContext(NavigationContext);
52
+ let { hash, pathname, search } = useResolvedPath(to);
53
+
54
+ let joinedPathname = pathname;
55
+ if (basename !== "/") {
56
+ let toPathname = getToPathname(to);
57
+ let endsWithSlash = toPathname != null && toPathname.endsWith("/");
58
+ joinedPathname =
59
+ pathname === "/"
60
+ ? basename + (endsWithSlash ? "/" : "")
61
+ : joinPaths([basename, pathname]);
62
+ }
63
+
64
+ return navigator.createHref({ pathname: joinedPathname, search, hash });
65
+ }
66
+
67
+ /**
68
+ * Returns true if this component is a descendant of a <Router>.
69
+ *
70
+ * @see https://reactrouter.com/docs/en/v6/hooks/use-in-router-context
71
+ */
72
+ export function useInRouterContext(): boolean {
73
+ return React.useContext(LocationContext) != null;
74
+ }
75
+
76
+ /**
77
+ * Returns the current location object, which represents the current URL in web
78
+ * browsers.
79
+ *
80
+ * Note: If you're using this it may mean you're doing some of your own
81
+ * "routing" in your app, and we'd like to know what your use case is. We may
82
+ * be able to provide something higher-level to better suit your needs.
83
+ *
84
+ * @see https://reactrouter.com/docs/en/v6/hooks/use-location
85
+ */
86
+ export function useLocation(): Location {
87
+ invariant(
88
+ useInRouterContext(),
89
+ // TODO: This error is probably because they somehow have 2 versions of the
90
+ // router loaded. We can help them understand how to avoid that.
91
+ `useLocation() may be used only in the context of a <Router> component.`
92
+ );
93
+
94
+ return React.useContext(LocationContext).location;
95
+ }
96
+
97
+ /**
98
+ * Returns the current navigation action which describes how the router came to
99
+ * the current location, either by a pop, push, or replace on the history stack.
100
+ *
101
+ * @see https://reactrouter.com/docs/en/v6/hooks/use-navigation-type
102
+ */
103
+ export function useNavigationType(): NavigationType {
104
+ return React.useContext(LocationContext).navigationType;
105
+ }
106
+
107
+ /**
108
+ * Returns true if the URL for the given "to" value matches the current URL.
109
+ * This is useful for components that need to know "active" state, e.g.
110
+ * <NavLink>.
111
+ *
112
+ * @see https://reactrouter.com/docs/en/v6/hooks/use-match
113
+ */
114
+ export function useMatch<
115
+ ParamKey extends ParamParseKey<Path>,
116
+ Path extends string
117
+ >(pattern: PathPattern<Path> | Path): PathMatch<ParamKey> | null {
118
+ invariant(
119
+ useInRouterContext(),
120
+ // TODO: This error is probably because they somehow have 2 versions of the
121
+ // router loaded. We can help them understand how to avoid that.
122
+ `useMatch() may be used only in the context of a <Router> component.`
123
+ );
124
+
125
+ let { pathname } = useLocation();
126
+ return React.useMemo(
127
+ () => matchPath<ParamKey, Path>(pattern, pathname),
128
+ [pathname, pattern]
129
+ );
130
+ }
131
+
132
+ /**
133
+ * The interface for the navigate() function returned from useNavigate().
134
+ */
135
+ export interface NavigateFunction {
136
+ (to: To, options?: NavigateOptions): void;
137
+ (delta: number): void;
138
+ }
139
+
140
+ /**
141
+ * Returns an imperative method for changing the location. Used by <Link>s, but
142
+ * may also be used by other elements to change the location.
143
+ *
144
+ * @see https://reactrouter.com/docs/en/v6/hooks/use-navigate
145
+ */
146
+ export function useNavigate(): NavigateFunction {
147
+ invariant(
148
+ useInRouterContext(),
149
+ // TODO: This error is probably because they somehow have 2 versions of the
150
+ // router loaded. We can help them understand how to avoid that.
151
+ `useNavigate() may be used only in the context of a <Router> component.`
152
+ );
153
+
154
+ let { basename, navigator } = React.useContext(NavigationContext);
155
+ let { matches } = React.useContext(RouteContext);
156
+ let { pathname: locationPathname } = useLocation();
157
+
158
+ let routePathnamesJson = JSON.stringify(
159
+ matches.map((match) => match.pathnameBase)
160
+ );
161
+
162
+ let activeRef = React.useRef(false);
163
+ React.useEffect(() => {
164
+ activeRef.current = true;
165
+ });
166
+
167
+ let navigate: NavigateFunction = React.useCallback(
168
+ (to: To | number, options: NavigateOptions = {}) => {
169
+ warning(
170
+ activeRef.current,
171
+ `You should call navigate() in a React.useEffect(), not when ` +
172
+ `your component is first rendered.`
173
+ );
174
+
175
+ if (!activeRef.current) return;
176
+
177
+ if (typeof to === "number") {
178
+ navigator.go(to);
179
+ return;
180
+ }
181
+
182
+ let path = resolveTo(
183
+ to,
184
+ JSON.parse(routePathnamesJson),
185
+ locationPathname
186
+ );
187
+
188
+ if (basename !== "/") {
189
+ path.pathname = joinPaths([basename, path.pathname]);
190
+ }
191
+
192
+ (!!options.replace ? navigator.replace : navigator.push)(
193
+ path,
194
+ options.state,
195
+ options
196
+ );
197
+ },
198
+ [basename, navigator, routePathnamesJson, locationPathname]
199
+ );
200
+
201
+ return navigate;
202
+ }
203
+
204
+ const OutletContext = React.createContext<unknown>(null);
205
+
206
+ /**
207
+ * Returns the context (if provided) for the child route at this level of the route
208
+ * hierarchy.
209
+ * @see https://reactrouter.com/docs/en/v6/hooks/use-outlet-context
210
+ */
211
+ export function useOutletContext<Context = unknown>(): Context {
212
+ return React.useContext(OutletContext) as Context;
213
+ }
214
+
215
+ /**
216
+ * Returns the element for the child route at this level of the route
217
+ * hierarchy. Used internally by <Outlet> to render child routes.
218
+ *
219
+ * @see https://reactrouter.com/docs/en/v6/hooks/use-outlet
220
+ */
221
+ export function useOutlet(context?: unknown): React.ReactElement | null {
222
+ let outlet = React.useContext(RouteContext).outlet;
223
+ if (outlet) {
224
+ return (
225
+ <OutletContext.Provider value={context}>{outlet}</OutletContext.Provider>
226
+ );
227
+ }
228
+ return outlet;
229
+ }
230
+
231
+ /**
232
+ * Returns an object of key/value pairs of the dynamic params from the current
233
+ * URL that were matched by the route path.
234
+ *
235
+ * @see https://reactrouter.com/docs/en/v6/hooks/use-params
236
+ */
237
+ export function useParams<
238
+ ParamsOrKey extends string | Record<string, string | undefined> = string
239
+ >(): Readonly<
240
+ [ParamsOrKey] extends [string] ? Params<ParamsOrKey> : Partial<ParamsOrKey>
241
+ > {
242
+ let { matches } = React.useContext(RouteContext);
243
+ let routeMatch = matches[matches.length - 1];
244
+ return routeMatch ? (routeMatch.params as any) : {};
245
+ }
246
+
247
+ /**
248
+ * Resolves the pathname of the given `to` value against the current location.
249
+ *
250
+ * @see https://reactrouter.com/docs/en/v6/hooks/use-resolved-path
251
+ */
252
+ export function useResolvedPath(to: To): Path {
253
+ let { matches } = React.useContext(RouteContext);
254
+ let { pathname: locationPathname } = useLocation();
255
+
256
+ let routePathnamesJson = JSON.stringify(
257
+ matches.map((match) => match.pathnameBase)
258
+ );
259
+
260
+ return React.useMemo(
261
+ () => resolveTo(to, JSON.parse(routePathnamesJson), locationPathname),
262
+ [to, routePathnamesJson, locationPathname]
263
+ );
264
+ }
265
+
266
+ /**
267
+ * Returns the element of the route that matched the current location, prepared
268
+ * with the correct context to render the remainder of the route tree. Route
269
+ * elements in the tree must render an <Outlet> to render their child route's
270
+ * element.
271
+ *
272
+ * @see https://reactrouter.com/docs/en/v6/hooks/use-routes
273
+ */
274
+ export function useRoutes(
275
+ routes: RouteObject[],
276
+ locationArg?: Partial<Location> | string
277
+ ): React.ReactElement | null {
278
+ invariant(
279
+ useInRouterContext(),
280
+ // TODO: This error is probably because they somehow have 2 versions of the
281
+ // router loaded. We can help them understand how to avoid that.
282
+ `useRoutes() may be used only in the context of a <Router> component.`
283
+ );
284
+
285
+ let dataRouterStateContext = React.useContext(DataRouterStateContext);
286
+ let { matches: parentMatches } = React.useContext(RouteContext);
287
+ let routeMatch = parentMatches[parentMatches.length - 1];
288
+ let parentParams = routeMatch ? routeMatch.params : {};
289
+ let parentPathname = routeMatch ? routeMatch.pathname : "/";
290
+ let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";
291
+ let parentRoute = routeMatch && routeMatch.route;
292
+
293
+ if (__DEV__) {
294
+ // You won't get a warning about 2 different <Routes> under a <Route>
295
+ // without a trailing *, but this is a best-effort warning anyway since we
296
+ // cannot even give the warning unless they land at the parent route.
297
+ //
298
+ // Example:
299
+ //
300
+ // <Routes>
301
+ // {/* This route path MUST end with /* because otherwise
302
+ // it will never match /blog/post/123 */}
303
+ // <Route path="blog" element={<Blog />} />
304
+ // <Route path="blog/feed" element={<BlogFeed />} />
305
+ // </Routes>
306
+ //
307
+ // function Blog() {
308
+ // return (
309
+ // <Routes>
310
+ // <Route path="post/:id" element={<Post />} />
311
+ // </Routes>
312
+ // );
313
+ // }
314
+ let parentPath = (parentRoute && parentRoute.path) || "";
315
+ warningOnce(
316
+ parentPathname,
317
+ !parentRoute || parentPath.endsWith("*"),
318
+ `You rendered descendant <Routes> (or called \`useRoutes()\`) at ` +
319
+ `"${parentPathname}" (under <Route path="${parentPath}">) but the ` +
320
+ `parent route path has no trailing "*". This means if you navigate ` +
321
+ `deeper, the parent won't match anymore and therefore the child ` +
322
+ `routes will never render.\n\n` +
323
+ `Please change the parent <Route path="${parentPath}"> to <Route ` +
324
+ `path="${parentPath === "/" ? "*" : `${parentPath}/*`}">.`
325
+ );
326
+ }
327
+
328
+ let locationFromContext = useLocation();
329
+
330
+ let location;
331
+ if (locationArg) {
332
+ let parsedLocationArg =
333
+ typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
334
+
335
+ invariant(
336
+ parentPathnameBase === "/" ||
337
+ parsedLocationArg.pathname?.startsWith(parentPathnameBase),
338
+ `When overriding the location using \`<Routes location>\` or \`useRoutes(routes, location)\`, ` +
339
+ `the location pathname must begin with the portion of the URL pathname that was ` +
340
+ `matched by all parent routes. The current pathname base is "${parentPathnameBase}" ` +
341
+ `but pathname "${parsedLocationArg.pathname}" was given in the \`location\` prop.`
342
+ );
343
+
344
+ location = parsedLocationArg;
345
+ } else {
346
+ location = locationFromContext;
347
+ }
348
+
349
+ let pathname = location.pathname || "/";
350
+ let remainingPathname =
351
+ parentPathnameBase === "/"
352
+ ? pathname
353
+ : pathname.slice(parentPathnameBase.length) || "/";
354
+
355
+ let matches = matchRoutes(routes, { pathname: remainingPathname });
356
+
357
+ if (__DEV__) {
358
+ warning(
359
+ parentRoute || matches != null,
360
+ `No routes matched location "${location.pathname}${location.search}${location.hash}" `
361
+ );
362
+
363
+ warning(
364
+ matches == null ||
365
+ matches[matches.length - 1].route.element !== undefined,
366
+ `Matched leaf route at location "${location.pathname}${location.search}${location.hash}" does not have an element. ` +
367
+ `This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.`
368
+ );
369
+ }
370
+
371
+ return _renderMatches(
372
+ matches &&
373
+ matches.map((match) =>
374
+ Object.assign({}, match, {
375
+ params: Object.assign({}, parentParams, match.params),
376
+ pathname: joinPaths([parentPathnameBase, match.pathname]),
377
+ pathnameBase:
378
+ match.pathnameBase === "/"
379
+ ? parentPathnameBase
380
+ : joinPaths([parentPathnameBase, match.pathnameBase]),
381
+ })
382
+ ),
383
+ parentMatches,
384
+ dataRouterStateContext || undefined
385
+ );
386
+ }
387
+
388
+ function DefaultErrorElement() {
389
+ let error = useRouteError();
390
+ let message = isRouteErrorResponse(error)
391
+ ? `${error.status} ${error.statusText}`
392
+ : error?.message || JSON.stringify(error);
393
+ let lightgrey = "rgba(200,200,200, 0.5)";
394
+ let preStyles = { padding: "0.5rem", backgroundColor: lightgrey };
395
+ let codeStyles = { padding: "2px 4px", backgroundColor: lightgrey };
396
+ return (
397
+ <>
398
+ <h2>Unhandled Thrown Error!</h2>
399
+ <h3 style={{ fontStyle: "italic" }}>{message}</h3>
400
+ {error?.stack ? <pre style={preStyles}>{error?.stack}</pre> : null}
401
+ <p>💿 Hey developer 👋</p>
402
+ <p>
403
+ You can provide a way better UX than this when your app throws errors by
404
+ providing your own&nbsp;
405
+ <code style={codeStyles}>errorElement</code> props on&nbsp;
406
+ <code style={codeStyles}>&lt;Route&gt;</code>
407
+ </p>
408
+ </>
409
+ );
410
+ }
411
+
412
+ type RenderErrorBoundaryProps = React.PropsWithChildren<{
413
+ location: Location;
414
+ error: any;
415
+ component: React.ReactNode;
416
+ }>;
417
+
418
+ type RenderErrorBoundaryState = {
419
+ location: Location;
420
+ error: any;
421
+ };
422
+
423
+ export class RenderErrorBoundary extends React.Component<
424
+ RenderErrorBoundaryProps,
425
+ RenderErrorBoundaryState
426
+ > {
427
+ constructor(props: RenderErrorBoundaryProps) {
428
+ super(props);
429
+ this.state = {
430
+ location: props.location,
431
+ error: props.error,
432
+ };
433
+ }
434
+
435
+ static getDerivedStateFromError(error: any) {
436
+ return { error: error };
437
+ }
438
+
439
+ static getDerivedStateFromProps(
440
+ props: RenderErrorBoundaryProps,
441
+ state: RenderErrorBoundaryState
442
+ ) {
443
+ // When we get into an error state, the user will likely click "back" to the
444
+ // previous page that didn't have an error. Because this wraps the entire
445
+ // application, that will have no effect--the error page continues to display.
446
+ // This gives us a mechanism to recover from the error when the location changes.
447
+ //
448
+ // Whether we're in an error state or not, we update the location in state
449
+ // so that when we are in an error state, it gets reset when a new location
450
+ // comes in and the user recovers from the error.
451
+ if (state.location !== props.location) {
452
+ return {
453
+ error: props.error,
454
+ location: props.location,
455
+ };
456
+ }
457
+
458
+ // If we're not changing locations, preserve the location but still surface
459
+ // any new errors that may come through. We retain the existing error, we do
460
+ // this because the error provided from the app state may be cleared without
461
+ // the location changing.
462
+ return {
463
+ error: props.error || state.error,
464
+ location: state.location,
465
+ };
466
+ }
467
+
468
+ componentDidCatch(error: any, errorInfo: any) {
469
+ console.error(
470
+ "React Router caught the following error during render",
471
+ error,
472
+ errorInfo
473
+ );
474
+ }
475
+
476
+ render() {
477
+ return this.state.error ? (
478
+ <RouteErrorContext.Provider
479
+ value={this.state.error}
480
+ children={this.props.component}
481
+ />
482
+ ) : (
483
+ this.props.children
484
+ );
485
+ }
486
+ }
487
+
488
+ export function _renderMatches(
489
+ matches: RouteMatch[] | null,
490
+ parentMatches: RouteMatch[] = [],
491
+ dataRouterState?: DataRouter["state"]
492
+ ): React.ReactElement | null {
493
+ if (matches == null) {
494
+ if (dataRouterState?.errors) {
495
+ // Don't bail if we have data router errors so we can render them in the
496
+ // boundary. Use the pre-matched (or shimmed) matches
497
+ matches = dataRouterState.matches;
498
+ } else {
499
+ return null;
500
+ }
501
+ }
502
+
503
+ let renderedMatches = matches;
504
+
505
+ // If we have data errors, trim matches to the highest error boundary
506
+ let errors = dataRouterState?.errors;
507
+ if (errors != null) {
508
+ let errorIndex = renderedMatches.findIndex(
509
+ (m) => m.route.id && errors?.[m.route.id]
510
+ );
511
+ invariant(
512
+ errorIndex >= 0,
513
+ `Could not find a matching route for the current errors: ${errors}`
514
+ );
515
+ renderedMatches = renderedMatches.slice(
516
+ 0,
517
+ Math.min(renderedMatches.length, errorIndex + 1)
518
+ );
519
+ }
520
+
521
+ return renderedMatches.reduceRight((outlet, match, index) => {
522
+ let error = match.route.id ? errors?.[match.route.id] : null;
523
+ // Only data routers handle errors
524
+ let errorElement = dataRouterState
525
+ ? match.route.errorElement || <DefaultErrorElement />
526
+ : null;
527
+ let getChildren = () => (
528
+ <RouteContext.Provider
529
+ children={
530
+ error
531
+ ? errorElement
532
+ : match.route.element !== undefined
533
+ ? match.route.element
534
+ : outlet
535
+ }
536
+ value={{
537
+ outlet,
538
+ matches: parentMatches.concat(renderedMatches.slice(0, index + 1)),
539
+ }}
540
+ />
541
+ );
542
+
543
+ // Only wrap in an error boundary within data router usages when we have an
544
+ // errorElement on this route. Otherwise let it bubble up to an ancestor
545
+ // errorElement
546
+ return dataRouterState && (match.route.errorElement || index === 0) ? (
547
+ <RenderErrorBoundary
548
+ location={dataRouterState.location}
549
+ component={errorElement}
550
+ error={error}
551
+ children={getChildren()}
552
+ />
553
+ ) : (
554
+ getChildren()
555
+ );
556
+ }, null as React.ReactElement | null);
557
+ }
558
+
559
+ enum DataRouterHook {
560
+ UseLoaderData = "useLoaderData",
561
+ UseActionData = "useActionData",
562
+ UseRouteError = "useRouteError",
563
+ UseNavigation = "useNavigation",
564
+ UseRouteLoaderData = "useRouteLoaderData",
565
+ UseMatches = "useMatches",
566
+ UseRevalidator = "useRevalidator",
567
+ }
568
+
569
+ function useDataRouterState(hookName: DataRouterHook) {
570
+ let state = React.useContext(DataRouterStateContext);
571
+ invariant(state, `${hookName} must be used within a DataRouter`);
572
+ return state;
573
+ }
574
+
575
+ /**
576
+ * Returns the current navigation, defaulting to an "idle" navigation when
577
+ * no navigation is in progress
578
+ */
579
+ export function useNavigation() {
580
+ let state = useDataRouterState(DataRouterHook.UseNavigation);
581
+ return state.navigation;
582
+ }
583
+
584
+ /**
585
+ * Returns a revalidate function for manually triggering revalidation, as well
586
+ * as the current state of any manual revalidations
587
+ */
588
+ export function useRevalidator() {
589
+ let router = React.useContext(DataRouterContext);
590
+ invariant(router, `useRevalidator must be used within a DataRouter`);
591
+ let state = useDataRouterState(DataRouterHook.UseRevalidator);
592
+ return { revalidate: router.revalidate, state: state.revalidation };
593
+ }
594
+
595
+ /**
596
+ * Returns the active route matches, useful for accessing loaderData for
597
+ * parent/child routes or the route "handle" property
598
+ */
599
+ export function useMatches() {
600
+ let { matches, loaderData } = useDataRouterState(DataRouterHook.UseMatches);
601
+ return React.useMemo(
602
+ () =>
603
+ matches.map((match) => {
604
+ let { pathname, params } = match;
605
+ return {
606
+ id: match.route.id,
607
+ pathname,
608
+ params,
609
+ data: loaderData[match.route.id],
610
+ handle: match.route.handle,
611
+ };
612
+ }),
613
+ [matches, loaderData]
614
+ );
615
+ }
616
+
617
+ /**
618
+ * Returns the loader data for the nearest ancestor Route loader
619
+ */
620
+ export function useLoaderData() {
621
+ let state = useDataRouterState(DataRouterHook.UseLoaderData);
622
+
623
+ let route = React.useContext(RouteContext);
624
+ invariant(route, `useLoaderData must be used inside a RouteContext`);
625
+
626
+ let thisRoute = route.matches[route.matches.length - 1];
627
+ invariant(
628
+ thisRoute.route.id,
629
+ `useLoaderData can only be used on routes that contain a unique "id"`
630
+ );
631
+
632
+ return state.loaderData?.[thisRoute.route.id];
633
+ }
634
+
635
+ /**
636
+ * Returns the loaderData for the given routeId
637
+ */
638
+ export function useRouteLoaderData(routeId: string): any {
639
+ let state = useDataRouterState(DataRouterHook.UseRouteLoaderData);
640
+ return state.loaderData?.[routeId];
641
+ }
642
+
643
+ /**
644
+ * Returns the action data for the nearest ancestor Route action
645
+ */
646
+ export function useActionData() {
647
+ let state = useDataRouterState(DataRouterHook.UseActionData);
648
+
649
+ let route = React.useContext(RouteContext);
650
+ invariant(route, `useActionData must be used inside a RouteContext`);
651
+
652
+ return Object.values(state?.actionData || {})[0];
653
+ }
654
+
655
+ /**
656
+ * Returns the nearest ancestor Route error, which could be a loader/action
657
+ * error or a render error. This is intended to be called from your
658
+ * errorElement to display a proper error message.
659
+ */
660
+ export function useRouteError() {
661
+ let error = React.useContext(RouteErrorContext);
662
+ let state = useDataRouterState(DataRouterHook.UseRouteError);
663
+ let route = React.useContext(RouteContext);
664
+ let thisRoute = route.matches[route.matches.length - 1];
665
+
666
+ // If this was a render error, we put it in a RouteError context inside
667
+ // of RenderErrorBoundary
668
+ if (error) {
669
+ return error;
670
+ }
671
+
672
+ invariant(route, `useRouteError must be used inside a RouteContext`);
673
+ invariant(
674
+ thisRoute.route.id,
675
+ `useRouteError can only be used on routes that contain a unique "id"`
676
+ );
677
+
678
+ // Otherwise look for errors from our data router state
679
+ return state.errors?.[thisRoute.route.id];
680
+ }
681
+
682
+ const alreadyWarned: Record<string, boolean> = {};
683
+
684
+ function warningOnce(key: string, cond: boolean, message: string) {
685
+ if (!cond && !alreadyWarned[key]) {
686
+ alreadyWarned[key] = true;
687
+ warning(false, message);
688
+ }
689
+ }