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.
- package/.eslintrc +12 -0
- package/CHANGELOG.md +8 -0
- package/__tests__/.eslintrc +8 -0
- package/__tests__/DataMemoryRouter-test.tsx +1902 -0
- package/__tests__/Route-test.tsx +45 -0
- package/__tests__/Router-basename-test.tsx +110 -0
- package/__tests__/Router-test.tsx +62 -0
- package/__tests__/Routes-location-test.tsx +69 -0
- package/__tests__/Routes-test.tsx +148 -0
- package/__tests__/__snapshots__/route-matching-test.tsx.snap +197 -0
- package/__tests__/absolute-path-matching-test.tsx +61 -0
- package/__tests__/createRoutesFromChildren-test.tsx +189 -0
- package/__tests__/descendant-routes-params-test.tsx +67 -0
- package/__tests__/descendant-routes-splat-matching-test.tsx +241 -0
- package/__tests__/descendant-routes-warning-test.tsx +140 -0
- package/__tests__/generatePath-test.tsx +45 -0
- package/__tests__/gh-issue-8127-test.tsx +32 -0
- package/__tests__/gh-issue-8165-test.tsx +97 -0
- package/__tests__/greedy-matching-test.tsx +89 -0
- package/__tests__/index-routes-test.tsx +24 -0
- package/__tests__/layout-routes-test.tsx +283 -0
- package/__tests__/matchPath-test.tsx +335 -0
- package/__tests__/matchRoutes-test.tsx +144 -0
- package/__tests__/navigate-test.tsx +49 -0
- package/__tests__/params-decode-test.tsx +36 -0
- package/__tests__/path-matching-test.tsx +270 -0
- package/__tests__/resolvePath-test.tsx +50 -0
- package/__tests__/route-depth-order-matching-test.tsx +135 -0
- package/__tests__/route-matching-test.tsx +164 -0
- package/__tests__/same-component-lifecycle-test.tsx +57 -0
- package/__tests__/setup.ts +15 -0
- package/__tests__/useHref-basename-test.tsx +351 -0
- package/__tests__/useHref-test.tsx +287 -0
- package/__tests__/useLocation-test.tsx +29 -0
- package/__tests__/useMatch-test.tsx +137 -0
- package/__tests__/useNavigate-test.tsx +100 -0
- package/__tests__/useOutlet-test.tsx +355 -0
- package/__tests__/useParams-test.tsx +212 -0
- package/__tests__/useResolvedPath-test.tsx +109 -0
- package/__tests__/useRoutes-test.tsx +122 -0
- package/__tests__/utils/renderStrict.tsx +21 -0
- package/__tests__/utils/waitForRedirect.tsx +5 -0
- package/index.ts +187 -0
- package/jest-transformer.js +10 -0
- package/jest.config.js +10 -0
- package/lib/components.tsx +491 -0
- package/lib/context.ts +96 -0
- package/lib/hooks.tsx +689 -0
- package/lib/use-sync-external-store-shim/index.ts +31 -0
- package/lib/use-sync-external-store-shim/useSyncExternalStoreShimClient.ts +153 -0
- package/lib/use-sync-external-store-shim/useSyncExternalStoreShimServer.ts +20 -0
- package/node-main.js +7 -0
- package/package.json +7 -4
- package/tsconfig.json +20 -0
- package/LICENSE.md +0 -22
- package/index.d.ts +0 -14
- package/index.js +0 -941
- package/index.js.map +0 -1
- package/lib/components.d.ts +0 -110
- package/lib/context.d.ts +0 -31
- package/lib/hooks.d.ts +0 -99
- package/lib/router.d.ts +0 -120
- package/main.js +0 -19
- package/react-router.development.js +0 -895
- package/react-router.development.js.map +0 -1
- package/react-router.production.min.js +0 -12
- package/react-router.production.min.js.map +0 -1
- package/umd/react-router.development.js +0 -990
- package/umd/react-router.development.js.map +0 -1
- package/umd/react-router.production.min.js +0 -12
- 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
|
|
405
|
+
<code style={codeStyles}>errorElement</code> props on
|
|
406
|
+
<code style={codeStyles}><Route></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
|
+
}
|