@remix-run/router 1.2.1 → 1.3.0-pre.1

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
@@ -26,6 +26,8 @@ export interface SuccessResult {
26
26
  export interface DeferredResult {
27
27
  type: ResultType.deferred;
28
28
  deferredData: DeferredData;
29
+ statusCode?: number;
30
+ headers?: Headers;
29
31
  }
30
32
  /**
31
33
  * Redirect result from a loader or action
@@ -158,7 +160,7 @@ export declare type AgnosticDataNonIndexRouteObject = AgnosticNonIndexRouteObjec
158
160
  * A data route object, which is just a RouteObject with a required unique ID
159
161
  */
160
162
  export declare type AgnosticDataRouteObject = AgnosticDataIndexRouteObject | AgnosticDataNonIndexRouteObject;
161
- declare type _PathParam<Path extends string> = Path extends `${infer L}/${infer R}` ? _PathParam<L> | _PathParam<R> : Path extends `:${infer Param}` ? Param : never;
163
+ 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;
162
164
  /**
163
165
  * Examples:
164
166
  * "/a/b/*" -> "*"
@@ -214,7 +216,7 @@ export declare function matchRoutes<RouteObjectType extends AgnosticRouteObject
214
216
  * @see https://reactrouter.com/utils/generate-path
215
217
  */
216
218
  export declare function generatePath<Path extends string>(originalPath: Path, params?: {
217
- [key in PathParam<Path>]: string;
219
+ [key in PathParam<Path>]: string | null;
218
220
  }): string;
219
221
  /**
220
222
  * A PathPattern is used to match on some portion of a URL pathname.
@@ -340,22 +342,27 @@ export interface TrackedPromise extends Promise<any> {
340
342
  export declare class AbortedDeferredError extends Error {
341
343
  }
342
344
  export declare class DeferredData {
343
- private pendingKeys;
345
+ private pendingKeysSet;
344
346
  private controller;
345
347
  private abortPromise;
346
348
  private unlistenAbortSignal;
347
- private subscriber?;
349
+ private subscribers;
348
350
  data: Record<string, unknown>;
349
- constructor(data: Record<string, unknown>);
351
+ init?: ResponseInit;
352
+ deferredKeys: string[];
353
+ constructor(data: Record<string, unknown>, responseInit?: ResponseInit);
350
354
  private trackPromise;
351
355
  private onSettle;
352
- subscribe(fn: (aborted: boolean) => void): void;
356
+ private emit;
357
+ subscribe(fn: (aborted: boolean, settledKey?: string) => void): () => boolean;
353
358
  cancel(): void;
354
359
  resolveData(signal: AbortSignal): Promise<boolean>;
355
360
  get done(): boolean;
356
361
  get unwrappedData(): {};
362
+ get pendingKeys(): string[];
357
363
  }
358
- export declare function defer(data: Record<string, unknown>): DeferredData;
364
+ export declare type DeferFunction = (data: Record<string, unknown>, init?: number | ResponseInit) => DeferredData;
365
+ export declare const defer: DeferFunction;
359
366
  export declare type RedirectFunction = (url: string, init?: number | ResponseInit) => Response;
360
367
  /**
361
368
  * A redirect response. Sets the status code and the `Location` header.
package/history.ts CHANGED
@@ -125,6 +125,13 @@ export interface History {
125
125
  */
126
126
  createHref(to: To): string;
127
127
 
128
+ /**
129
+ * Returns a URL for the given `to` value
130
+ *
131
+ * @param to - The destination URL
132
+ */
133
+ createURL(to: To): URL;
134
+
128
135
  /**
129
136
  * Encode a location the same way window.history would do (no-op for memory
130
137
  * history) so we ensure our PUSH/REPLACE navigations for data routers
@@ -255,6 +262,10 @@ export function createMemoryHistory(
255
262
  return location;
256
263
  }
257
264
 
265
+ function createHref(to: To) {
266
+ return typeof to === "string" ? to : createPath(to);
267
+ }
268
+
258
269
  let history: MemoryHistory = {
259
270
  get index() {
260
271
  return index;
@@ -265,8 +276,9 @@ export function createMemoryHistory(
265
276
  get location() {
266
277
  return getCurrentLocation();
267
278
  },
268
- createHref(to) {
269
- return typeof to === "string" ? to : createPath(to);
279
+ createHref,
280
+ createURL(to) {
281
+ return new URL(createHref(to), "http://localhost");
270
282
  },
271
283
  encodeLocation(to: To) {
272
284
  let path = typeof to === "string" ? parsePath(to) : to;
@@ -558,24 +570,6 @@ export function parsePath(path: string): Partial<Path> {
558
570
  return parsedPath;
559
571
  }
560
572
 
561
- export function createClientSideURL(location: Location | string): URL {
562
- // window.location.origin is "null" (the literal string value) in Firefox
563
- // under certain conditions, notably when serving from a local HTML file
564
- // See https://bugzilla.mozilla.org/show_bug.cgi?id=878297
565
- let base =
566
- typeof window !== "undefined" &&
567
- typeof window.location !== "undefined" &&
568
- window.location.origin !== "null"
569
- ? window.location.origin
570
- : window.location.href;
571
- let href = typeof location === "string" ? location : createPath(location);
572
- invariant(
573
- base,
574
- `No window.location.(origin|href) available to create URL for href: ${href}`
575
- );
576
- return new URL(href, base);
577
- }
578
-
579
573
  export interface UrlHistory extends History {}
580
574
 
581
575
  export type UrlHistoryOptions = {
@@ -637,6 +631,23 @@ function getUrlBasedHistory(
637
631
  }
638
632
  }
639
633
 
634
+ function createURL(to: To): URL {
635
+ // window.location.origin is "null" (the literal string value) in Firefox
636
+ // under certain conditions, notably when serving from a local HTML file
637
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=878297
638
+ let base =
639
+ window.location.origin !== "null"
640
+ ? window.location.origin
641
+ : window.location.href;
642
+
643
+ let href = typeof to === "string" ? to : createPath(to);
644
+ invariant(
645
+ base,
646
+ `No window.location.(origin|href) available to create URL for href: ${href}`
647
+ );
648
+ return new URL(href, base);
649
+ }
650
+
640
651
  let history: History = {
641
652
  get action() {
642
653
  return action;
@@ -659,11 +670,10 @@ function getUrlBasedHistory(
659
670
  createHref(to) {
660
671
  return createHref(window, to);
661
672
  },
673
+ createURL,
662
674
  encodeLocation(to) {
663
675
  // Encode a Location the same way window.location would
664
- let url = createClientSideURL(
665
- typeof to === "string" ? to : createPath(to)
666
- );
676
+ let url = createURL(to);
667
677
  return {
668
678
  pathname: url.pathname,
669
679
  search: url.search,
package/index.ts CHANGED
@@ -1,5 +1,3 @@
1
- import { convertRoutesToDataRoutes, getPathContributingMatches } from "./utils";
2
-
3
1
  export type {
4
2
  ActionFunction,
5
3
  ActionFunctionArgs,
@@ -58,6 +56,7 @@ export type {
58
56
  Path,
59
57
  To,
60
58
  } from "./history";
59
+
61
60
  export {
62
61
  Action,
63
62
  createBrowserHistory,
@@ -79,6 +78,7 @@ export * from "./router";
79
78
 
80
79
  /** @internal */
81
80
  export {
81
+ DeferredData as UNSAFE_DeferredData,
82
82
  convertRoutesToDataRoutes as UNSAFE_convertRoutesToDataRoutes,
83
83
  getPathContributingMatches as UNSAFE_getPathContributingMatches,
84
- };
84
+ } from "./utils";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/router",
3
- "version": "1.2.1",
3
+ "version": "1.3.0-pre.1",
4
4
  "description": "Nested/Data-driven/Framework-agnostic Routing",
5
5
  "keywords": [
6
6
  "remix",
package/router.ts CHANGED
@@ -3,7 +3,6 @@ import {
3
3
  Action as HistoryAction,
4
4
  createLocation,
5
5
  createPath,
6
- createClientSideURL,
7
6
  invariant,
8
7
  parsePath,
9
8
  } from "./history";
@@ -308,6 +307,7 @@ export interface StaticHandlerContext {
308
307
  statusCode: number;
309
308
  loaderHeaders: Record<string, Headers>;
310
309
  actionHeaders: Record<string, Headers>;
310
+ activeDeferreds: Record<string, DeferredData> | null;
311
311
  _deepestRenderedBoundaryId?: string | null;
312
312
  }
313
313
 
@@ -371,6 +371,7 @@ type LinkNavigateOptions = {
371
371
  type SubmissionNavigateOptions = {
372
372
  replace?: boolean;
373
373
  state?: any;
374
+ preventScrollReset?: boolean;
374
375
  formMethod?: FormMethod;
375
376
  formEncType?: FormEncType;
376
377
  formData: FormData;
@@ -771,6 +772,14 @@ export function createRouter(init: RouterInit): Router {
771
772
  )
772
773
  : state.loaderData;
773
774
 
775
+ // Always respect the user flag. Otherwise don't reset on mutation
776
+ // submission navigations unless they redirect
777
+ let preventScrollReset =
778
+ pendingPreventScrollReset === true ||
779
+ (state.navigation.formMethod != null &&
780
+ isMutationMethod(state.navigation.formMethod) &&
781
+ location.state?._isRedirect !== true);
782
+
774
783
  updateState({
775
784
  ...newState, // matches, errors, fetchers go through as-is
776
785
  actionData,
@@ -780,11 +789,11 @@ export function createRouter(init: RouterInit): Router {
780
789
  initialized: true,
781
790
  navigation: IDLE_NAVIGATION,
782
791
  revalidation: "idle",
783
- // Don't restore on submission navigations
784
- restoreScrollPosition: state.navigation.formData
785
- ? false
786
- : getSavedScrollPosition(location, newState.matches || state.matches),
787
- preventScrollReset: pendingPreventScrollReset,
792
+ restoreScrollPosition: getSavedScrollPosition(
793
+ location,
794
+ newState.matches || state.matches
795
+ ),
796
+ preventScrollReset,
788
797
  });
789
798
 
790
799
  if (isUninterruptedRevalidation) {
@@ -957,6 +966,7 @@ export function createRouter(init: RouterInit): Router {
957
966
  // Create a controller/Request for this navigation
958
967
  pendingNavigationController = new AbortController();
959
968
  let request = createClientSideRequest(
969
+ init.history,
960
970
  location,
961
971
  pendingNavigationController.signal,
962
972
  opts && opts.submission
@@ -1115,7 +1125,7 @@ export function createRouter(init: RouterInit): Router {
1115
1125
  }
1116
1126
 
1117
1127
  if (isDeferredResult(result)) {
1118
- throw new Error("defer() is not supported in actions");
1128
+ throw getInternalRouterError(400, { type: "defer-action" });
1119
1129
  }
1120
1130
 
1121
1131
  return {
@@ -1167,6 +1177,7 @@ export function createRouter(init: RouterInit): Router {
1167
1177
  : undefined;
1168
1178
 
1169
1179
  let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
1180
+ init.history,
1170
1181
  state,
1171
1182
  matches,
1172
1183
  activeSubmission,
@@ -1380,6 +1391,7 @@ export function createRouter(init: RouterInit): Router {
1380
1391
  // Call the action for the fetcher
1381
1392
  let abortController = new AbortController();
1382
1393
  let fetchRequest = createClientSideRequest(
1394
+ init.history,
1383
1395
  path,
1384
1396
  abortController.signal,
1385
1397
  submission
@@ -1427,13 +1439,15 @@ export function createRouter(init: RouterInit): Router {
1427
1439
  }
1428
1440
 
1429
1441
  if (isDeferredResult(actionResult)) {
1430
- invariant(false, "defer() is not supported in actions");
1442
+ throw getInternalRouterError(400, { type: "defer-action" });
1431
1443
  }
1432
1444
 
1433
1445
  // Start the data load for current matches, or the next location if we're
1434
1446
  // in the middle of a navigation
1435
1447
  let nextLocation = state.navigation.location || state.location;
1436
1448
  let revalidationRequest = createClientSideRequest(
1449
+ init.history,
1450
+
1437
1451
  nextLocation,
1438
1452
  abortController.signal
1439
1453
  );
@@ -1456,6 +1470,7 @@ export function createRouter(init: RouterInit): Router {
1456
1470
  state.fetchers.set(key, loadFetcher);
1457
1471
 
1458
1472
  let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
1473
+ init.history,
1459
1474
  state,
1460
1475
  matches,
1461
1476
  submission,
@@ -1599,7 +1614,11 @@ export function createRouter(init: RouterInit): Router {
1599
1614
 
1600
1615
  // Call the loader for this fetcher route match
1601
1616
  let abortController = new AbortController();
1602
- let fetchRequest = createClientSideRequest(path, abortController.signal);
1617
+ let fetchRequest = createClientSideRequest(
1618
+ init.history,
1619
+ path,
1620
+ abortController.signal
1621
+ );
1603
1622
  fetchControllers.set(key, abortController);
1604
1623
  let result: DataResult = await callLoaderOrAction(
1605
1624
  "loader",
@@ -1609,7 +1628,7 @@ export function createRouter(init: RouterInit): Router {
1609
1628
  router.basename
1610
1629
  );
1611
1630
 
1612
- // Deferred isn't supported or fetcher loads, await everything and treat it
1631
+ // Deferred isn't supported for fetcher loads, await everything and treat it
1613
1632
  // as a normal load. resolveDeferredData will return undefined if this
1614
1633
  // fetcher gets aborted, so we just leave result untouched and short circuit
1615
1634
  // below if that happens
@@ -1719,7 +1738,7 @@ export function createRouter(init: RouterInit): Router {
1719
1738
 
1720
1739
  // Check if this an external redirect that goes to a new origin
1721
1740
  if (typeof window?.location !== "undefined") {
1722
- let newOrigin = createClientSideURL(redirect.location).origin;
1741
+ let newOrigin = init.history.createURL(redirect.location).origin;
1723
1742
  if (window.location.origin !== newOrigin) {
1724
1743
  if (replace) {
1725
1744
  window.location.replace(redirect.location);
@@ -1762,6 +1781,8 @@ export function createRouter(init: RouterInit): Router {
1762
1781
  ...submission,
1763
1782
  formAction: redirect.location,
1764
1783
  },
1784
+ // Preserve this flag across redirects
1785
+ preventScrollReset: pendingPreventScrollReset,
1765
1786
  });
1766
1787
  } else {
1767
1788
  // Otherwise, we kick off a new loading navigation, preserving the
@@ -1775,6 +1796,8 @@ export function createRouter(init: RouterInit): Router {
1775
1796
  formEncType: submission ? submission.formEncType : undefined,
1776
1797
  formData: submission ? submission.formData : undefined,
1777
1798
  },
1799
+ // Preserve this flag across redirects
1800
+ preventScrollReset: pendingPreventScrollReset,
1778
1801
  });
1779
1802
  }
1780
1803
  }
@@ -1796,7 +1819,7 @@ export function createRouter(init: RouterInit): Router {
1796
1819
  ...fetchersToLoad.map(([, href, match, fetchMatches]) =>
1797
1820
  callLoaderOrAction(
1798
1821
  "loader",
1799
- createClientSideRequest(href, request.signal),
1822
+ createClientSideRequest(init.history, href, request.signal),
1800
1823
  match,
1801
1824
  fetchMatches,
1802
1825
  router.basename
@@ -2027,6 +2050,8 @@ export function createRouter(init: RouterInit): Router {
2027
2050
  //#region createStaticHandler
2028
2051
  ////////////////////////////////////////////////////////////////////////////////
2029
2052
 
2053
+ export const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
2054
+
2030
2055
  export function createStaticHandler(
2031
2056
  routes: AgnosticRouteObject[],
2032
2057
  opts?: {
@@ -2086,6 +2111,7 @@ export function createStaticHandler(
2086
2111
  statusCode: error.status,
2087
2112
  loaderHeaders: {},
2088
2113
  actionHeaders: {},
2114
+ activeDeferreds: null,
2089
2115
  };
2090
2116
  } else if (!matches) {
2091
2117
  let error = getInternalRouterError(404, { pathname: location.pathname });
@@ -2103,6 +2129,7 @@ export function createStaticHandler(
2103
2129
  statusCode: error.status,
2104
2130
  loaderHeaders: {},
2105
2131
  actionHeaders: {},
2132
+ activeDeferreds: null,
2106
2133
  };
2107
2134
  }
2108
2135
 
@@ -2191,8 +2218,19 @@ export function createStaticHandler(
2191
2218
  }
2192
2219
 
2193
2220
  // Pick off the right state value to return
2194
- let routeData = [result.actionData, result.loaderData].find((v) => v);
2195
- return Object.values(routeData || {})[0];
2221
+ if (result.actionData) {
2222
+ return Object.values(result.actionData)[0];
2223
+ }
2224
+
2225
+ if (result.loaderData) {
2226
+ let data = Object.values(result.loaderData)[0];
2227
+ if (result.activeDeferreds?.[match.route.id]) {
2228
+ data[UNSAFE_DEFERRED_SYMBOL] = result.activeDeferreds[match.route.id];
2229
+ }
2230
+ return data;
2231
+ }
2232
+
2233
+ return undefined;
2196
2234
  }
2197
2235
 
2198
2236
  async function queryImpl(
@@ -2305,7 +2343,14 @@ export function createStaticHandler(
2305
2343
  }
2306
2344
 
2307
2345
  if (isDeferredResult(result)) {
2308
- throw new Error("defer() is not supported in actions");
2346
+ let error = getInternalRouterError(400, { type: "defer-action" });
2347
+ if (isRouteRequest) {
2348
+ throw error;
2349
+ }
2350
+ result = {
2351
+ type: ResultType.error,
2352
+ error,
2353
+ };
2309
2354
  }
2310
2355
 
2311
2356
  if (isRouteRequest) {
@@ -2325,6 +2370,7 @@ export function createStaticHandler(
2325
2370
  statusCode: 200,
2326
2371
  loaderHeaders: {},
2327
2372
  actionHeaders: {},
2373
+ activeDeferreds: null,
2328
2374
  };
2329
2375
  }
2330
2376
 
@@ -2420,6 +2466,7 @@ export function createStaticHandler(
2420
2466
  errors: pendingActionError || null,
2421
2467
  statusCode: 200,
2422
2468
  loaderHeaders: {},
2469
+ activeDeferreds: null,
2423
2470
  };
2424
2471
  }
2425
2472
 
@@ -2443,25 +2490,20 @@ export function createStaticHandler(
2443
2490
  throw new Error(`${method}() call aborted`);
2444
2491
  }
2445
2492
 
2446
- let executedLoaders = new Set<string>();
2447
- results.forEach((result, i) => {
2448
- executedLoaders.add(matchesToLoad[i].route.id);
2449
- // Can't do anything with these without the Remix side of things, so just
2450
- // cancel them for now
2451
- if (isDeferredResult(result)) {
2452
- result.deferredData.cancel();
2453
- }
2454
- });
2455
-
2456
2493
  // Process and commit output from loaders
2494
+ let activeDeferreds = new Map<string, DeferredData>();
2457
2495
  let context = processRouteLoaderData(
2458
2496
  matches,
2459
2497
  matchesToLoad,
2460
2498
  results,
2461
- pendingActionError
2499
+ pendingActionError,
2500
+ activeDeferreds
2462
2501
  );
2463
2502
 
2464
2503
  // Add a null for any non-loader matches for proper revalidation on the client
2504
+ let executedLoaders = new Set<string>(
2505
+ matchesToLoad.map((match) => match.route.id)
2506
+ );
2465
2507
  matches.forEach((match) => {
2466
2508
  if (!executedLoaders.has(match.route.id)) {
2467
2509
  context.loaderData[match.route.id] = null;
@@ -2471,6 +2513,10 @@ export function createStaticHandler(
2471
2513
  return {
2472
2514
  ...context,
2473
2515
  matches,
2516
+ activeDeferreds:
2517
+ activeDeferreds.size > 0
2518
+ ? Object.fromEntries(activeDeferreds.entries())
2519
+ : null,
2474
2520
  };
2475
2521
  }
2476
2522
 
@@ -2595,6 +2641,7 @@ function getLoaderMatchesUntilBoundary(
2595
2641
  }
2596
2642
 
2597
2643
  function getMatchesToLoad(
2644
+ history: History,
2598
2645
  state: RouterState,
2599
2646
  matches: AgnosticDataRouteMatch[],
2600
2647
  submission: Submission | undefined,
@@ -2622,6 +2669,7 @@ function getMatchesToLoad(
2622
2669
  // If this route had a pending deferred cancelled it must be revalidated
2623
2670
  cancelledDeferredRoutes.some((id) => id === match.route.id) ||
2624
2671
  shouldRevalidateLoader(
2672
+ history,
2625
2673
  state.location,
2626
2674
  state.matches[index],
2627
2675
  submission,
@@ -2641,6 +2689,7 @@ function getMatchesToLoad(
2641
2689
  revalidatingFetchers.push([key, href, match, fetchMatches]);
2642
2690
  } else if (isRevalidationRequired) {
2643
2691
  let shouldRevalidate = shouldRevalidateLoader(
2692
+ history,
2644
2693
  href,
2645
2694
  match,
2646
2695
  submission,
@@ -2694,6 +2743,7 @@ function isNewRouteInstance(
2694
2743
  }
2695
2744
 
2696
2745
  function shouldRevalidateLoader(
2746
+ history: History,
2697
2747
  currentLocation: string | Location,
2698
2748
  currentMatch: AgnosticDataRouteMatch,
2699
2749
  submission: Submission | undefined,
@@ -2702,9 +2752,9 @@ function shouldRevalidateLoader(
2702
2752
  isRevalidationRequired: boolean,
2703
2753
  actionResult: DataResult | undefined
2704
2754
  ) {
2705
- let currentUrl = createClientSideURL(currentLocation);
2755
+ let currentUrl = history.createURL(currentLocation);
2706
2756
  let currentParams = currentMatch.params;
2707
- let nextUrl = createClientSideURL(location);
2757
+ let nextUrl = history.createURL(location);
2708
2758
  let nextParams = match.params;
2709
2759
 
2710
2760
  // This is the default implementation as to when we revalidate. If the route
@@ -2795,8 +2845,7 @@ async function callLoaderOrAction(
2795
2845
  "Redirects returned/thrown from loaders/actions must have a Location header"
2796
2846
  );
2797
2847
 
2798
- let isAbsolute =
2799
- /^[a-z+]+:\/\//i.test(location) || location.startsWith("//");
2848
+ let isAbsolute = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(location);
2800
2849
 
2801
2850
  // Support relative routing in internal redirects
2802
2851
  if (!isAbsolute) {
@@ -2893,11 +2942,12 @@ async function callLoaderOrAction(
2893
2942
  // client-side navigations and fetches. During SSR we will always have a
2894
2943
  // Request instance from the static handler (query/queryRoute)
2895
2944
  function createClientSideRequest(
2945
+ history: History,
2896
2946
  location: string | Location,
2897
2947
  signal: AbortSignal,
2898
2948
  submission?: Submission
2899
2949
  ): Request {
2900
- let url = createClientSideURL(stripHashFromPath(location)).toString();
2950
+ let url = history.createURL(stripHashFromPath(location)).toString();
2901
2951
  let init: RequestInit = { signal };
2902
2952
 
2903
2953
  if (submission && isMutationMethod(submission.formMethod)) {
@@ -2933,7 +2983,7 @@ function processRouteLoaderData(
2933
2983
  matchesToLoad: AgnosticDataRouteMatch[],
2934
2984
  results: DataResult[],
2935
2985
  pendingError: RouteData | undefined,
2936
- activeDeferreds?: Map<string, DeferredData>
2986
+ activeDeferreds: Map<string, DeferredData>
2937
2987
  ): {
2938
2988
  loaderData: RouterState["loaderData"];
2939
2989
  errors: RouterState["errors"] | null;
@@ -2988,12 +3038,14 @@ function processRouteLoaderData(
2988
3038
  if (result.headers) {
2989
3039
  loaderHeaders[id] = result.headers;
2990
3040
  }
2991
- } else if (isDeferredResult(result)) {
2992
- activeDeferreds && activeDeferreds.set(id, result.deferredData);
2993
- loaderData[id] = result.deferredData.data;
2994
- // TODO: Add statusCode/headers once we wire up streaming in Remix
2995
3041
  } else {
2996
- loaderData[id] = result.data;
3042
+ if (isDeferredResult(result)) {
3043
+ activeDeferreds.set(id, result.deferredData);
3044
+ loaderData[id] = result.deferredData.data;
3045
+ } else {
3046
+ loaderData[id] = result.data;
3047
+ }
3048
+
2997
3049
  // Error status codes always override success status codes, but if all
2998
3050
  // loaders are successful we take the deepest status code.
2999
3051
  if (
@@ -3068,11 +3120,11 @@ function processLoaderData(
3068
3120
  } else if (isRedirectResult(result)) {
3069
3121
  // Should never get here, redirects should get processed above, but we
3070
3122
  // keep this to type narrow to a success result in the else
3071
- throw new Error("Unhandled fetcher revalidation redirect");
3123
+ invariant(false, "Unhandled fetcher revalidation redirect");
3072
3124
  } else if (isDeferredResult(result)) {
3073
3125
  // Should never get here, deferred data should be awaited for fetchers
3074
3126
  // in resolveDeferredResults
3075
- throw new Error("Unhandled fetcher deferred data");
3127
+ invariant(false, "Unhandled fetcher deferred data");
3076
3128
  } else {
3077
3129
  let doneFetcher: FetcherStates["Idle"] = {
3078
3130
  state: "idle",
@@ -3163,10 +3215,12 @@ function getInternalRouterError(
3163
3215
  pathname,
3164
3216
  routeId,
3165
3217
  method,
3218
+ type,
3166
3219
  }: {
3167
3220
  pathname?: string;
3168
3221
  routeId?: string;
3169
3222
  method?: string;
3223
+ type?: "defer-action";
3170
3224
  } = {}
3171
3225
  ) {
3172
3226
  let statusText = "Unknown Server Error";
@@ -3179,6 +3233,8 @@ function getInternalRouterError(
3179
3233
  `You made a ${method} request to "${pathname}" but ` +
3180
3234
  `did not provide a \`loader\` for route "${routeId}", ` +
3181
3235
  `so there is no way to handle the request.`;
3236
+ } else if (type === "defer-action") {
3237
+ errorMessage = "defer() is not supported in actions";
3182
3238
  } else {
3183
3239
  errorMessage = "Cannot submit binary form data using GET";
3184
3240
  }