@remix-run/router 1.3.3 → 1.4.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.
package/dist/utils.d.ts CHANGED
@@ -117,6 +117,27 @@ export interface ShouldRevalidateFunction {
117
117
  defaultShouldRevalidate: boolean;
118
118
  }): boolean;
119
119
  }
120
+ /**
121
+ * Function provided by the framework-aware layers to set `hasErrorBoundary`
122
+ * from the framework-aware `errorElement` prop
123
+ */
124
+ export interface DetectErrorBoundaryFunction {
125
+ (route: AgnosticRouteObject): boolean;
126
+ }
127
+ /**
128
+ * Keys we cannot change from within a lazy() function. We spread all other keys
129
+ * onto the route. Either they're meaningful to the router, or they'll get
130
+ * ignored.
131
+ */
132
+ export declare type ImmutableRouteKey = "lazy" | "caseSensitive" | "path" | "id" | "index" | "children";
133
+ export declare const immutableRouteKeys: Set<ImmutableRouteKey>;
134
+ /**
135
+ * lazy() function to load a route definition, which can add non-matching
136
+ * related properties to a route
137
+ */
138
+ export interface LazyRouteFunction<R extends AgnosticRouteObject> {
139
+ (): Promise<Omit<R, ImmutableRouteKey>>;
140
+ }
120
141
  /**
121
142
  * Base RouteObject with common props shared by all types of routes
122
143
  */
@@ -129,6 +150,7 @@ declare type AgnosticBaseRouteObject = {
129
150
  hasErrorBoundary?: boolean;
130
151
  shouldRevalidate?: ShouldRevalidateFunction;
131
152
  handle?: any;
153
+ lazy?: LazyRouteFunction<AgnosticBaseRouteObject>;
132
154
  };
133
155
  /**
134
156
  * Index routes must not have children
@@ -160,6 +182,7 @@ export declare type AgnosticDataNonIndexRouteObject = AgnosticNonIndexRouteObjec
160
182
  * A data route object, which is just a RouteObject with a required unique ID
161
183
  */
162
184
  export declare type AgnosticDataRouteObject = AgnosticDataIndexRouteObject | AgnosticDataNonIndexRouteObject;
185
+ export declare type RouteManifest = Record<string, AgnosticDataRouteObject | undefined>;
163
186
  declare type _PathParam<Path extends string> = Path extends `${infer L}/${infer R}` ? _PathParam<L> | _PathParam<R> : Path extends `:${infer Param}` ? Param extends `${infer Optional}?` ? Optional : Param : never;
164
187
  /**
165
188
  * Examples:
@@ -170,7 +193,7 @@ declare type _PathParam<Path extends string> = Path extends `${infer L}/${infer
170
193
  * "/:a/:b" -> "a" | "b"
171
194
  * "/:a/b/:c/*" -> "a" | "c" | "*"
172
195
  */
173
- declare type PathParam<Path extends string> = Path extends "*" ? "*" : Path extends `${infer Rest}/*` ? "*" | _PathParam<Rest> : _PathParam<Path>;
196
+ declare type PathParam<Path extends string> = Path extends "*" | "/*" ? "*" : Path extends `${infer Rest}/*` ? "*" | _PathParam<Rest> : _PathParam<Path>;
174
197
  export declare type ParamParseKey<Segment extends string> = [
175
198
  PathParam<Segment>
176
199
  ] extends [never] ? string : PathParam<Segment>;
@@ -203,7 +226,7 @@ export interface AgnosticRouteMatch<ParamKey extends string = string, RouteObjec
203
226
  }
204
227
  export interface AgnosticDataRouteMatch extends AgnosticRouteMatch<string, AgnosticDataRouteObject> {
205
228
  }
206
- export declare function convertRoutesToDataRoutes(routes: AgnosticRouteObject[], parentPath?: number[], allIds?: Set<string>): AgnosticDataRouteObject[];
229
+ export declare function convertRoutesToDataRoutes(routes: AgnosticRouteObject[], detectErrorBoundary: DetectErrorBoundaryFunction, parentPath?: number[], manifest?: RouteManifest): AgnosticDataRouteObject[];
207
230
  /**
208
231
  * Matches the given routes to a location and returns the match data.
209
232
  *
@@ -270,10 +293,6 @@ export declare function matchPath<ParamKey extends ParamParseKey<Path>, Path ext
270
293
  * @private
271
294
  */
272
295
  export declare function stripBasename(pathname: string, basename: string): string | null;
273
- /**
274
- * @private
275
- */
276
- export declare function warning(cond: any, message: string): void;
277
296
  /**
278
297
  * Returns a resolved path object relative to the given pathname.
279
298
  *
package/history.ts CHANGED
@@ -481,7 +481,7 @@ export function invariant(value: any, message?: string) {
481
481
  }
482
482
  }
483
483
 
484
- function warning(cond: any, message: string) {
484
+ export function warning(cond: any, message: string) {
485
485
  if (!cond) {
486
486
  // eslint-disable-next-line no-console
487
487
  if (typeof console !== "undefined") console.warn(message);
package/index.ts CHANGED
@@ -9,6 +9,7 @@ export type {
9
9
  AgnosticNonIndexRouteObject,
10
10
  AgnosticRouteMatch,
11
11
  AgnosticRouteObject,
12
+ LazyRouteFunction,
12
13
  TrackedPromise,
13
14
  FormEncType,
14
15
  FormMethod,
@@ -40,7 +41,6 @@ export {
40
41
  resolvePath,
41
42
  resolveTo,
42
43
  stripBasename,
43
- warning,
44
44
  } from "./utils";
45
45
 
46
46
  export type {
@@ -76,10 +76,14 @@ export * from "./router";
76
76
  ///////////////////////////////////////////////////////////////////////////////
77
77
 
78
78
  /** @internal */
79
+ export type { RouteManifest as UNSAFE_RouteManifest } from "./utils";
79
80
  export {
80
81
  DeferredData as UNSAFE_DeferredData,
81
82
  convertRoutesToDataRoutes as UNSAFE_convertRoutesToDataRoutes,
82
83
  getPathContributingMatches as UNSAFE_getPathContributingMatches,
83
84
  } from "./utils";
84
85
 
85
- export { invariant as UNSAFE_invariant } from "./history";
86
+ export {
87
+ invariant as UNSAFE_invariant,
88
+ warning as UNSAFE_warning,
89
+ } from "./history";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/router",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "Nested/Data-driven/Framework-agnostic Routing",
5
5
  "keywords": [
6
6
  "remix",
package/router.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  createPath,
6
6
  invariant,
7
7
  parsePath,
8
+ warning,
8
9
  } from "./history";
9
10
  import type {
10
11
  DataResult,
@@ -14,6 +15,7 @@ import type {
14
15
  ErrorResult,
15
16
  FormEncType,
16
17
  FormMethod,
18
+ DetectErrorBoundaryFunction,
17
19
  RedirectResult,
18
20
  RouteData,
19
21
  AgnosticRouteObject,
@@ -22,6 +24,10 @@ import type {
22
24
  AgnosticRouteMatch,
23
25
  MutationFormMethod,
24
26
  ShouldRevalidateFunction,
27
+ RouteManifest,
28
+ ImmutableRouteKey,
29
+ ActionFunction,
30
+ LoaderFunction,
25
31
  } from "./utils";
26
32
  import {
27
33
  DeferredData,
@@ -29,12 +35,12 @@ import {
29
35
  ResultType,
30
36
  convertRoutesToDataRoutes,
31
37
  getPathContributingMatches,
38
+ immutableRouteKeys,
32
39
  isRouteErrorResponse,
33
40
  joinPaths,
34
41
  matchRoutes,
35
42
  resolveTo,
36
43
  stripBasename,
37
- warning,
38
44
  } from "./utils";
39
45
 
40
46
  ////////////////////////////////////////////////////////////////////////////////
@@ -328,6 +334,7 @@ export interface RouterInit {
328
334
  routes: AgnosticRouteObject[];
329
335
  history: History;
330
336
  hydrationData?: HydrationState;
337
+ detectErrorBoundary?: DetectErrorBoundaryFunction;
331
338
  }
332
339
 
333
340
  /**
@@ -638,6 +645,9 @@ const isBrowser =
638
645
  typeof window.document !== "undefined" &&
639
646
  typeof window.document.createElement !== "undefined";
640
647
  const isServer = !isBrowser;
648
+
649
+ const defaultDetectErrorBoundary = (route: AgnosticRouteObject) =>
650
+ Boolean(route.hasErrorBoundary);
641
651
  //#endregion
642
652
 
643
653
  ////////////////////////////////////////////////////////////////////////////////
@@ -653,7 +663,18 @@ export function createRouter(init: RouterInit): Router {
653
663
  "You must provide a non-empty routes array to createRouter"
654
664
  );
655
665
 
656
- let dataRoutes = convertRoutesToDataRoutes(init.routes);
666
+ let detectErrorBoundary =
667
+ init.detectErrorBoundary || defaultDetectErrorBoundary;
668
+
669
+ // Routes keyed by ID
670
+ let manifest: RouteManifest = {};
671
+ // Routes in tree format for matching
672
+ let dataRoutes = convertRoutesToDataRoutes(
673
+ init.routes,
674
+ detectErrorBoundary,
675
+ undefined,
676
+ manifest
677
+ );
657
678
  let inFlightDataRoutes: AgnosticDataRouteObject[] | undefined;
658
679
  // Cleanup function for history
659
680
  let unlistenHistory: (() => void) | null = null;
@@ -692,7 +713,11 @@ export function createRouter(init: RouterInit): Router {
692
713
  }
693
714
 
694
715
  let initialized =
695
- !initialMatches.some((m) => m.route.loader) || init.hydrationData != null;
716
+ // All initialMatches need to be loaded before we're ready. If we have lazy
717
+ // functions around still then we'll need to run them in initialize()
718
+ !initialMatches.some((m) => m.route.lazy) &&
719
+ // And we have to either have no loaders or have been provided hydrationData
720
+ (!initialMatches.some((m) => m.route.loader) || init.hydrationData != null);
696
721
 
697
722
  let router: Router;
698
723
  let state: RouterState = {
@@ -837,11 +862,35 @@ export function createRouter(init: RouterInit): Router {
837
862
  }
838
863
  );
839
864
 
840
- // Kick off initial data load if needed. Use Pop to avoid modifying history
841
- if (!state.initialized) {
865
+ if (state.initialized) {
866
+ return router;
867
+ }
868
+
869
+ let lazyMatches = state.matches.filter((m) => m.route.lazy);
870
+
871
+ if (lazyMatches.length === 0) {
872
+ // Kick off initial data load if needed. Use Pop to avoid modifying history
842
873
  startNavigation(HistoryAction.Pop, state.location);
874
+ return router;
843
875
  }
844
876
 
877
+ // Load lazy modules, then kick off initial data load if needed
878
+ let lazyPromises = lazyMatches.map((m) =>
879
+ loadLazyRouteModule(m.route, detectErrorBoundary, manifest)
880
+ );
881
+ Promise.all(lazyPromises).then(() => {
882
+ let initialized =
883
+ !state.matches.some((m) => m.route.loader) ||
884
+ init.hydrationData != null;
885
+ if (initialized) {
886
+ // We already have required loaderData so we can just set initialized
887
+ updateState({ initialized: true });
888
+ } else {
889
+ // We still need to kick off initial data loads
890
+ startNavigation(HistoryAction.Pop, state.location);
891
+ }
892
+ });
893
+
845
894
  return router;
846
895
  }
847
896
 
@@ -1259,7 +1308,7 @@ export function createRouter(init: RouterInit): Router {
1259
1308
  let result: DataResult;
1260
1309
  let actionMatch = getTargetMatch(matches, location);
1261
1310
 
1262
- if (!actionMatch.route.action) {
1311
+ if (!actionMatch.route.action && !actionMatch.route.lazy) {
1263
1312
  result = {
1264
1313
  type: ResultType.error,
1265
1314
  error: getInternalRouterError(405, {
@@ -1274,6 +1323,8 @@ export function createRouter(init: RouterInit): Router {
1274
1323
  request,
1275
1324
  actionMatch,
1276
1325
  matches,
1326
+ manifest,
1327
+ detectErrorBoundary,
1277
1328
  router.basename
1278
1329
  );
1279
1330
 
@@ -1566,7 +1617,7 @@ export function createRouter(init: RouterInit): Router {
1566
1617
  interruptActiveLoads();
1567
1618
  fetchLoadMatches.delete(key);
1568
1619
 
1569
- if (!match.route.action) {
1620
+ if (!match.route.action && !match.route.lazy) {
1570
1621
  let error = getInternalRouterError(405, {
1571
1622
  method: submission.formMethod,
1572
1623
  pathname: path,
@@ -1602,6 +1653,8 @@ export function createRouter(init: RouterInit): Router {
1602
1653
  fetchRequest,
1603
1654
  match,
1604
1655
  requestMatches,
1656
+ manifest,
1657
+ detectErrorBoundary,
1605
1658
  router.basename
1606
1659
  );
1607
1660
 
@@ -1821,11 +1874,14 @@ export function createRouter(init: RouterInit): Router {
1821
1874
  abortController.signal
1822
1875
  );
1823
1876
  fetchControllers.set(key, abortController);
1877
+
1824
1878
  let result: DataResult = await callLoaderOrAction(
1825
1879
  "loader",
1826
1880
  fetchRequest,
1827
1881
  match,
1828
1882
  matches,
1883
+ manifest,
1884
+ detectErrorBoundary,
1829
1885
  router.basename
1830
1886
  );
1831
1887
 
@@ -2021,7 +2077,15 @@ export function createRouter(init: RouterInit): Router {
2021
2077
  // accordingly
2022
2078
  let results = await Promise.all([
2023
2079
  ...matchesToLoad.map((match) =>
2024
- callLoaderOrAction("loader", request, match, matches, router.basename)
2080
+ callLoaderOrAction(
2081
+ "loader",
2082
+ request,
2083
+ match,
2084
+ matches,
2085
+ manifest,
2086
+ detectErrorBoundary,
2087
+ router.basename
2088
+ )
2025
2089
  ),
2026
2090
  ...fetchersToLoad.map((f) => {
2027
2091
  if (f.matches && f.match) {
@@ -2030,6 +2094,8 @@ export function createRouter(init: RouterInit): Router {
2030
2094
  createClientSideRequest(init.history, f.path, request.signal),
2031
2095
  f.match,
2032
2096
  f.matches,
2097
+ manifest,
2098
+ detectErrorBoundary,
2033
2099
  router.basename
2034
2100
  );
2035
2101
  } else {
@@ -2346,18 +2412,29 @@ export function createRouter(init: RouterInit): Router {
2346
2412
 
2347
2413
  export const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
2348
2414
 
2415
+ export interface CreateStaticHandlerOptions {
2416
+ basename?: string;
2417
+ detectErrorBoundary?: DetectErrorBoundaryFunction;
2418
+ }
2419
+
2349
2420
  export function createStaticHandler(
2350
2421
  routes: AgnosticRouteObject[],
2351
- opts?: {
2352
- basename?: string;
2353
- }
2422
+ opts?: CreateStaticHandlerOptions
2354
2423
  ): StaticHandler {
2355
2424
  invariant(
2356
2425
  routes.length > 0,
2357
2426
  "You must provide a non-empty routes array to createStaticHandler"
2358
2427
  );
2359
2428
 
2360
- let dataRoutes = convertRoutesToDataRoutes(routes);
2429
+ let manifest: RouteManifest = {};
2430
+ let detectErrorBoundary =
2431
+ opts?.detectErrorBoundary || defaultDetectErrorBoundary;
2432
+ let dataRoutes = convertRoutesToDataRoutes(
2433
+ routes,
2434
+ detectErrorBoundary,
2435
+ undefined,
2436
+ manifest
2437
+ );
2361
2438
  let basename = (opts ? opts.basename : null) || "/";
2362
2439
 
2363
2440
  /**
@@ -2592,7 +2669,7 @@ export function createStaticHandler(
2592
2669
  ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
2593
2670
  let result: DataResult;
2594
2671
 
2595
- if (!actionMatch.route.action) {
2672
+ if (!actionMatch.route.action && !actionMatch.route.lazy) {
2596
2673
  let error = getInternalRouterError(405, {
2597
2674
  method: request.method,
2598
2675
  pathname: new URL(request.url).pathname,
@@ -2611,6 +2688,8 @@ export function createStaticHandler(
2611
2688
  request,
2612
2689
  actionMatch,
2613
2690
  matches,
2691
+ manifest,
2692
+ detectErrorBoundary,
2614
2693
  basename,
2615
2694
  true,
2616
2695
  isRouteRequest,
@@ -2732,7 +2811,11 @@ export function createStaticHandler(
2732
2811
  let isRouteRequest = routeMatch != null;
2733
2812
 
2734
2813
  // Short circuit if we have no loaders to run (queryRoute())
2735
- if (isRouteRequest && !routeMatch?.route.loader) {
2814
+ if (
2815
+ isRouteRequest &&
2816
+ !routeMatch?.route.loader &&
2817
+ !routeMatch?.route.lazy
2818
+ ) {
2736
2819
  throw getInternalRouterError(400, {
2737
2820
  method: request.method,
2738
2821
  pathname: new URL(request.url).pathname,
@@ -2746,7 +2829,9 @@ export function createStaticHandler(
2746
2829
  matches,
2747
2830
  Object.keys(pendingActionError || {})[0]
2748
2831
  );
2749
- let matchesToLoad = requestMatches.filter((m) => m.route.loader);
2832
+ let matchesToLoad = requestMatches.filter(
2833
+ (m) => m.route.loader || m.route.lazy
2834
+ );
2750
2835
 
2751
2836
  // Short circuit if we have no loaders to run (query())
2752
2837
  if (matchesToLoad.length === 0) {
@@ -2771,6 +2856,8 @@ export function createStaticHandler(
2771
2856
  request,
2772
2857
  match,
2773
2858
  matches,
2859
+ manifest,
2860
+ detectErrorBoundary,
2774
2861
  basename,
2775
2862
  true,
2776
2863
  isRouteRequest,
@@ -2960,6 +3047,10 @@ function getMatchesToLoad(
2960
3047
  let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
2961
3048
 
2962
3049
  let navigationMatches = boundaryMatches.filter((match, index) => {
3050
+ if (match.route.lazy) {
3051
+ // We haven't loaded this route yet so we don't know if it's got a loader!
3052
+ return true;
3053
+ }
2963
3054
  if (match.route.loader == null) {
2964
3055
  return false;
2965
3056
  }
@@ -3096,11 +3187,90 @@ function shouldRevalidateLoader(
3096
3187
  return arg.defaultShouldRevalidate;
3097
3188
  }
3098
3189
 
3190
+ /**
3191
+ * Execute route.lazy() methods to lazily load route modules (loader, action,
3192
+ * shouldRevalidate) and update the routeManifest in place which shares objects
3193
+ * with dataRoutes so those get updated as well.
3194
+ */
3195
+ async function loadLazyRouteModule(
3196
+ route: AgnosticDataRouteObject,
3197
+ detectErrorBoundary: DetectErrorBoundaryFunction,
3198
+ manifest: RouteManifest
3199
+ ) {
3200
+ if (!route.lazy) {
3201
+ return;
3202
+ }
3203
+
3204
+ let lazyRoute = await route.lazy();
3205
+
3206
+ // If the lazy route function was executed and removed by another parallel
3207
+ // call then we can return - first lazy() to finish wins because the return
3208
+ // value of lazy is expected to be static
3209
+ if (!route.lazy) {
3210
+ return;
3211
+ }
3212
+
3213
+ let routeToUpdate = manifest[route.id];
3214
+ invariant(routeToUpdate, "No route found in manifest");
3215
+
3216
+ // Update the route in place. This should be safe because there's no way
3217
+ // we could yet be sitting on this route as we can't get there without
3218
+ // resolving lazy() first.
3219
+ //
3220
+ // This is different than the HMR "update" use-case where we may actively be
3221
+ // on the route being updated. The main concern boils down to "does this
3222
+ // mutation affect any ongoing navigations or any current state.matches
3223
+ // values?". If not, it should be safe to update in place.
3224
+ let routeUpdates: Record<string, any> = {};
3225
+ for (let lazyRouteProperty in lazyRoute) {
3226
+ let staticRouteValue =
3227
+ routeToUpdate[lazyRouteProperty as keyof typeof routeToUpdate];
3228
+
3229
+ let isPropertyStaticallyDefined =
3230
+ staticRouteValue !== undefined &&
3231
+ // This property isn't static since it should always be updated based
3232
+ // on the route updates
3233
+ lazyRouteProperty !== "hasErrorBoundary";
3234
+
3235
+ warning(
3236
+ !isPropertyStaticallyDefined,
3237
+ `Route "${routeToUpdate.id}" has a static property "${lazyRouteProperty}" ` +
3238
+ `defined but its lazy function is also returning a value for this property. ` +
3239
+ `The lazy route property "${lazyRouteProperty}" will be ignored.`
3240
+ );
3241
+
3242
+ if (
3243
+ !isPropertyStaticallyDefined &&
3244
+ !immutableRouteKeys.has(lazyRouteProperty as ImmutableRouteKey)
3245
+ ) {
3246
+ routeUpdates[lazyRouteProperty] =
3247
+ lazyRoute[lazyRouteProperty as keyof typeof lazyRoute];
3248
+ }
3249
+ }
3250
+
3251
+ // Mutate the route with the provided updates. Do this first so we pass
3252
+ // the updated version to detectErrorBoundary
3253
+ Object.assign(routeToUpdate, routeUpdates);
3254
+
3255
+ // Mutate the `hasErrorBoundary` property on the route based on the route
3256
+ // updates and remove the `lazy` function so we don't resolve the lazy
3257
+ // route again.
3258
+ Object.assign(routeToUpdate, {
3259
+ // To keep things framework agnostic, we use the provided
3260
+ // `detectErrorBoundary` function to set the `hasErrorBoundary` route
3261
+ // property since the logic will differ between frameworks.
3262
+ hasErrorBoundary: detectErrorBoundary({ ...routeToUpdate }),
3263
+ lazy: undefined,
3264
+ });
3265
+ }
3266
+
3099
3267
  async function callLoaderOrAction(
3100
3268
  type: "loader" | "action",
3101
3269
  request: Request,
3102
3270
  match: AgnosticDataRouteMatch,
3103
3271
  matches: AgnosticDataRouteMatch[],
3272
+ manifest: RouteManifest,
3273
+ detectErrorBoundary: DetectErrorBoundaryFunction,
3104
3274
  basename = "/",
3105
3275
  isStaticRequest: boolean = false,
3106
3276
  isRouteRequest: boolean = false,
@@ -3108,24 +3278,61 @@ async function callLoaderOrAction(
3108
3278
  ): Promise<DataResult> {
3109
3279
  let resultType;
3110
3280
  let result;
3111
-
3112
- // Setup a promise we can race against so that abort signals short circuit
3113
- let reject: () => void;
3114
- let abortPromise = new Promise((_, r) => (reject = r));
3115
- let onReject = () => reject();
3116
- request.signal.addEventListener("abort", onReject);
3281
+ let onReject: (() => void) | undefined;
3282
+
3283
+ let runHandler = (handler: ActionFunction | LoaderFunction) => {
3284
+ // Setup a promise we can race against so that abort signals short circuit
3285
+ let reject: () => void;
3286
+ let abortPromise = new Promise((_, r) => (reject = r));
3287
+ onReject = () => reject();
3288
+ request.signal.addEventListener("abort", onReject);
3289
+ return Promise.race([
3290
+ handler({ request, params: match.params, context: requestContext }),
3291
+ abortPromise,
3292
+ ]);
3293
+ };
3117
3294
 
3118
3295
  try {
3119
3296
  let handler = match.route[type];
3120
- invariant<Function>(
3121
- handler,
3122
- `Could not find the ${type} to run on the "${match.route.id}" route`
3123
- );
3124
3297
 
3125
- result = await Promise.race([
3126
- handler({ request, params: match.params, context: requestContext }),
3127
- abortPromise,
3128
- ]);
3298
+ if (match.route.lazy) {
3299
+ if (handler) {
3300
+ // Run statically defined handler in parallel with lazy()
3301
+ let values = await Promise.all([
3302
+ runHandler(handler),
3303
+ loadLazyRouteModule(match.route, detectErrorBoundary, manifest),
3304
+ ]);
3305
+ result = values[0];
3306
+ } else {
3307
+ // Load lazy route module, then run any returned handler
3308
+ await loadLazyRouteModule(match.route, detectErrorBoundary, manifest);
3309
+
3310
+ handler = match.route[type];
3311
+ if (handler) {
3312
+ // Handler still run even if we got interrupted to maintain consistency
3313
+ // with un-abortable behavior of handler execution on non-lazy or
3314
+ // previously-lazy-loaded routes
3315
+ result = await runHandler(handler);
3316
+ } else if (type === "action") {
3317
+ throw getInternalRouterError(405, {
3318
+ method: request.method,
3319
+ pathname: new URL(request.url).pathname,
3320
+ routeId: match.route.id,
3321
+ });
3322
+ } else {
3323
+ // lazy() route has no loader to run. Short circuit here so we don't
3324
+ // hit the invariant below that errors on returning undefined.
3325
+ return { type: ResultType.data, data: undefined };
3326
+ }
3327
+ }
3328
+ } else {
3329
+ invariant<Function>(
3330
+ handler,
3331
+ `Could not find the ${type} to run on the "${match.route.id}" route`
3332
+ );
3333
+
3334
+ result = await runHandler(handler);
3335
+ }
3129
3336
 
3130
3337
  invariant(
3131
3338
  result !== undefined,
@@ -3137,7 +3344,9 @@ async function callLoaderOrAction(
3137
3344
  resultType = ResultType.error;
3138
3345
  result = e;
3139
3346
  } finally {
3140
- request.signal.removeEventListener("abort", onReject);
3347
+ if (onReject) {
3348
+ request.signal.removeEventListener("abort", onReject);
3349
+ }
3141
3350
  }
3142
3351
 
3143
3352
  if (isResponse(result)) {