@tanstack/react-router 1.12.16 → 1.14.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.
@@ -9,11 +9,14 @@ import { AnyRouteMatch, MatchRouteOptions, RouteMatch } from './Matches.js';
9
9
  import { ParsedLocation } from './location.js';
10
10
  import { SearchSerializer, SearchParser } from './searchParams.js';
11
11
  import { BuildLocationFn, CommitLocationOptions, InjectedHtmlEntry, NavigateFn } from './RouterProvider.js';
12
+ import { NotFoundError } from './not-found.js';
12
13
  import { ResolveRelativePath, ToOptions } from './link.js';
13
14
  import { NoInfer } from '@tanstack/react-store';
14
15
  declare global {
15
16
  interface Window {
16
- __TSR_DEHYDRATED__?: HydrationCtx;
17
+ __TSR_DEHYDRATED__?: {
18
+ data: string;
19
+ };
17
20
  __TSR_ROUTER_CONTEXT__?: React.Context<Router<any>>;
18
21
  }
19
22
  }
@@ -61,8 +64,19 @@ export interface RouterOptions<TRouteTree extends AnyRoute, TDehydrated extends
61
64
  InnerWrap?: (props: {
62
65
  children: any;
63
66
  }) => JSX.Element;
67
+ /**
68
+ * @deprecated
69
+ * Use `notFoundComponent` instead.
70
+ * See https://tanstack.com/router/v1/docs/guide/not-found-errors#migrating-from-notfoundroute for more info.
71
+ */
64
72
  notFoundRoute?: AnyRoute;
73
+ transformer?: RouterTransformer;
65
74
  errorSerializer?: RouterErrorSerializer<TSerializedError>;
75
+ globalNotFound?: RouteComponent;
76
+ }
77
+ export interface RouterTransformer {
78
+ stringify: (obj: unknown) => string;
79
+ parse: (str: string) => unknown;
66
80
  }
67
81
  export interface RouterErrorSerializer<TSerializedError> {
68
82
  serialize: (err: unknown) => TSerializedError;
@@ -99,12 +113,12 @@ export interface BuildNextOptions {
99
113
  export interface DehydratedRouterState {
100
114
  dehydratedMatches: DehydratedRouteMatch[];
101
115
  }
102
- export type DehydratedRouteMatch = Pick<RouteMatch, 'id' | 'status' | 'updatedAt' | 'loaderData'>;
116
+ export type DehydratedRouteMatch = Pick<RouteMatch, 'id' | 'status' | 'updatedAt' | 'notFoundError' | 'loaderData'>;
103
117
  export interface DehydratedRouter {
104
118
  state: DehydratedRouterState;
105
119
  }
106
120
  export type RouterConstructorOptions<TRouteTree extends AnyRoute, TDehydrated extends Record<string, any>, TSerializedError extends Record<string, any>> = Omit<RouterOptions<TRouteTree, TDehydrated, TSerializedError>, 'context'> & RouterContextOptions<TRouteTree>;
107
- export declare const componentTypes: readonly ["component", "errorComponent", "pendingComponent"];
121
+ export declare const componentTypes: readonly ["component", "errorComponent", "pendingComponent", "notFoundComponent"];
108
122
  export type RouterEvents = {
109
123
  onBeforeLoad: {
110
124
  type: 'onBeforeLoad';
@@ -143,7 +157,9 @@ export declare class Router<TRouteTree extends AnyRoute = AnyRoute, TDehydrated
143
157
  injectedHtml: InjectedHtmlEntry[];
144
158
  dehydratedData?: TDehydrated;
145
159
  __store: Store<RouterState<TRouteTree>>;
146
- options: PickAsRequired<RouterOptions<TRouteTree, TDehydrated, TSerializedError>, 'stringifySearch' | 'parseSearch' | 'context'>;
160
+ options: PickAsRequired<Omit<RouterOptions<TRouteTree, TDehydrated, TSerializedError>, 'transformer'> & {
161
+ transformer: RouterTransformer;
162
+ }, 'stringifySearch' | 'parseSearch' | 'context'>;
147
163
  history: RouterHistory;
148
164
  latestLocation: ParsedLocation;
149
165
  basepath: string;
@@ -186,7 +202,9 @@ export declare class Router<TRouteTree extends AnyRoute = AnyRoute, TDehydrated
186
202
  dehydrateData: <T>(key: any, getData: T | (() => T | Promise<T>)) => () => T | undefined;
187
203
  hydrateData: <T extends unknown = unknown>(key: any) => T | undefined;
188
204
  dehydrate: () => DehydratedRouter;
189
- hydrate: (__do_not_use_server_ctx?: HydrationCtx) => Promise<void>;
205
+ hydrate: (__do_not_use_server_ctx?: string) => Promise<void>;
206
+ updateMatchesWithNotFound: (matches: AnyRouteMatch[], currentMatch: AnyRouteMatch, err: NotFoundError) => void;
207
+ hasNotFoundMatch: () => boolean;
190
208
  }
191
209
  export declare function lazyFn<T extends Record<string, (...args: any[]) => any>, TKey extends keyof T = 'default'>(fn: () => Promise<T>, key?: TKey): (...args: Parameters<T[TKey]>) => Promise<Awaited<ReturnType<T[TKey]>>>;
192
210
  export declare class SearchParamError extends Error {
@@ -1,15 +1,18 @@
1
1
  import { createBrowserHistory, createMemoryHistory } from "@tanstack/history";
2
2
  import { Store } from "@tanstack/react-store";
3
+ import { rootRouteId } from "./route.js";
3
4
  import { defaultStringifySearch, defaultParseSearch } from "./searchParams.js";
4
5
  import { replaceEqualDeep, pick, deepEqual, escapeJSON, last, functionalUpdate } from "./utils.js";
5
6
  import { getRouteMatch } from "./RouterProvider.js";
6
7
  import { trimPath, trimPathLeft, parsePathname, resolvePath, cleanPath, matchPathname, trimPathRight, interpolatePath, joinPaths } from "./path.js";
7
8
  import invariant from "tiny-invariant";
8
9
  import { isRedirect } from "./redirects.js";
10
+ import { isNotFound } from "./not-found.js";
9
11
  const componentTypes = [
10
12
  "component",
11
13
  "errorComponent",
12
- "pendingComponent"
14
+ "pendingComponent",
15
+ "notFoundComponent"
13
16
  ];
14
17
  function createRouter(options) {
15
18
  return new Router(options);
@@ -26,6 +29,11 @@ class Router {
26
29
  this.injectedHtml = [];
27
30
  this.startReactTransition = (fn) => fn();
28
31
  this.update = (newOptions) => {
32
+ if (newOptions.notFoundRoute) {
33
+ console.warn(
34
+ "The notFoundRoute API is deprecated and will be removed in the next major version. See https://tanstack.com/router/v1/docs/guide/not-found-errors#migrating-from-notfoundroute for more info."
35
+ );
36
+ }
29
37
  const previousOptions = this.options;
30
38
  this.options = {
31
39
  ...this.options,
@@ -209,15 +217,19 @@ class Router {
209
217
  });
210
218
  let routeCursor = foundRoute || this.routesById["__root__"];
211
219
  let matchedRoutes = [routeCursor];
220
+ let isGlobalNotFound = false;
212
221
  if (
213
222
  // If we found a route, and it's not an index route and we have left over path
214
- (foundRoute ? foundRoute.path !== "/" && routeParams["**"] : (
223
+ foundRoute ? foundRoute.path !== "/" && routeParams["**"] : (
215
224
  // Or if we didn't find a route and we have left over path
216
225
  trimPathRight(pathname)
217
- )) && // And we have a 404 route configured
218
- this.options.notFoundRoute
226
+ )
219
227
  ) {
220
- matchedRoutes.push(this.options.notFoundRoute);
228
+ if (this.options.notFoundRoute) {
229
+ matchedRoutes.push(this.options.notFoundRoute);
230
+ } else {
231
+ isGlobalNotFound = true;
232
+ }
221
233
  }
222
234
  while (routeCursor == null ? void 0 : routeCursor.parentRoute) {
223
235
  routeCursor = routeCursor.parentRoute;
@@ -287,7 +299,11 @@ class Router {
287
299
  var _a2;
288
300
  return (_a2 = route.options[d]) == null ? void 0 : _a2.preload;
289
301
  }));
290
- const match = existingMatch ? { ...existingMatch, cause } : {
302
+ const match = existingMatch ? {
303
+ ...existingMatch,
304
+ cause,
305
+ notFoundError: isGlobalNotFound && route.id === rootRouteId ? { global: true } : void 0
306
+ } : {
291
307
  id: matchId,
292
308
  routeId: route.id,
293
309
  params: routeParams,
@@ -309,6 +325,7 @@ class Router {
309
325
  loaderDeps,
310
326
  invalid: false,
311
327
  preload: false,
328
+ notFoundError: isGlobalNotFound && route.id === rootRouteId ? { global: true } : void 0,
312
329
  links: (_d = (_c = route.options).links) == null ? void 0 : _d.call(_c),
313
330
  scripts: (_f = (_e = route.options).scripts) == null ? void 0 : _f.call(_e),
314
331
  staticData: route.options.staticData || {}
@@ -546,6 +563,9 @@ class Router {
546
563
  if (isRedirect(err)) {
547
564
  throw err;
548
565
  }
566
+ if (isNotFound(err)) {
567
+ this.updateMatchesWithNotFound(matches, match, err);
568
+ }
549
569
  try {
550
570
  (_b2 = (_a2 = route.options).onError) == null ? void 0 : _b2.call(_a2, err);
551
571
  } catch (errorHandlerErr) {
@@ -629,6 +649,9 @@ class Router {
629
649
  }
630
650
  return true;
631
651
  }
652
+ if (isNotFound(err)) {
653
+ this.updateMatchesWithNotFound(matches, match, err);
654
+ }
632
655
  return false;
633
656
  };
634
657
  let loadPromise;
@@ -964,7 +987,7 @@ class Router {
964
987
  const data = typeof getData === "function" ? await getData() : getData;
965
988
  return `<script id='${id}' suppressHydrationWarning>window["__TSR_DEHYDRATED__${escapeJSON(
966
989
  strKey
967
- )}"] = ${JSON.stringify(data)}
990
+ )}"] = ${JSON.stringify(this.options.transformer.stringify(data))}
968
991
  ;(() => {
969
992
  var el = document.getElementById('${id}')
970
993
  el.parentElement.removeChild(el)
@@ -978,7 +1001,9 @@ class Router {
978
1001
  this.hydrateData = (key) => {
979
1002
  if (typeof document !== "undefined") {
980
1003
  const strKey = typeof key === "string" ? key : JSON.stringify(key);
981
- return window[`__TSR_DEHYDRATED__${strKey}`];
1004
+ return this.options.transformer.parse(
1005
+ window[`__TSR_DEHYDRATED__${strKey}`]
1006
+ );
982
1007
  }
983
1008
  return void 0;
984
1009
  };
@@ -988,7 +1013,15 @@ class Router {
988
1013
  return {
989
1014
  state: {
990
1015
  dehydratedMatches: this.state.matches.map((d) => ({
991
- ...pick(d, ["id", "status", "updatedAt", "loaderData"]),
1016
+ ...pick(d, [
1017
+ "id",
1018
+ "status",
1019
+ "updatedAt",
1020
+ "loaderData",
1021
+ // Not-founds that occur during SSR don't require the client to load data before
1022
+ // triggering in order to prevent the flicker of the loading component
1023
+ "notFoundError"
1024
+ ]),
992
1025
  // If an error occurs server-side during SSRing,
993
1026
  // send a small subset of the error to the client
994
1027
  error: d.error ? {
@@ -1000,24 +1033,24 @@ class Router {
1000
1033
  };
1001
1034
  };
1002
1035
  this.hydrate = async (__do_not_use_server_ctx) => {
1003
- var _a, _b;
1036
+ var _a, _b, _c;
1004
1037
  let _ctx = __do_not_use_server_ctx;
1005
1038
  if (typeof document !== "undefined") {
1006
- _ctx = window.__TSR_DEHYDRATED__;
1039
+ _ctx = (_a = window.__TSR_DEHYDRATED__) == null ? void 0 : _a.data;
1007
1040
  }
1008
1041
  invariant(
1009
1042
  _ctx,
1010
1043
  "Expected to find a __TSR_DEHYDRATED__ property on window... but we did not. Did you forget to render <DehydrateRouter /> in your app?"
1011
1044
  );
1012
- const ctx = _ctx;
1045
+ const ctx = this.options.transformer.parse(_ctx);
1013
1046
  this.dehydratedData = ctx.payload;
1014
- (_b = (_a = this.options).hydrate) == null ? void 0 : _b.call(_a, ctx.payload);
1047
+ (_c = (_b = this.options).hydrate) == null ? void 0 : _c.call(_b, ctx.payload);
1015
1048
  const dehydratedState = ctx.router.state;
1016
1049
  let matches = this.matchRoutes(
1017
1050
  this.state.location.pathname,
1018
1051
  this.state.location.search
1019
1052
  ).map((match) => {
1020
- var _a2, _b2, _c, _d, _e, _f;
1053
+ var _a2, _b2, _c2, _d, _e, _f;
1021
1054
  const dehydratedMatch = dehydratedState.dehydratedMatches.find(
1022
1055
  (d) => d.id === match.id
1023
1056
  );
@@ -1033,7 +1066,7 @@ class Router {
1033
1066
  meta: (_b2 = (_a2 = route.options).meta) == null ? void 0 : _b2.call(_a2, {
1034
1067
  loaderData: dehydratedMatch.loaderData
1035
1068
  }),
1036
- links: (_d = (_c = route.options).links) == null ? void 0 : _d.call(_c),
1069
+ links: (_d = (_c2 = route.options).links) == null ? void 0 : _d.call(_c2),
1037
1070
  scripts: (_f = (_e = route.options).scripts) == null ? void 0 : _f.call(_e)
1038
1071
  };
1039
1072
  }
@@ -1047,6 +1080,31 @@ class Router {
1047
1080
  };
1048
1081
  });
1049
1082
  };
1083
+ this.updateMatchesWithNotFound = (matches, currentMatch, err) => {
1084
+ const matchesByRouteId = Object.fromEntries(
1085
+ matches.map((match) => [match.routeId, match])
1086
+ );
1087
+ if (err.global) {
1088
+ matchesByRouteId[rootRouteId].notFoundError = err;
1089
+ } else {
1090
+ let currentRoute = this.routesById[err.route ?? currentMatch.routeId];
1091
+ while (!currentRoute.options.notFoundComponent) {
1092
+ currentRoute = currentRoute == null ? void 0 : currentRoute.parentRoute;
1093
+ invariant(
1094
+ currentRoute,
1095
+ "Found invalid route tree while trying to find not-found handler."
1096
+ );
1097
+ if (currentRoute.id === rootRouteId)
1098
+ break;
1099
+ }
1100
+ const match = matchesByRouteId[currentRoute.id];
1101
+ invariant(match, "Could not find match for route: " + currentRoute.id);
1102
+ match.notFoundError = err;
1103
+ }
1104
+ };
1105
+ this.hasNotFoundMatch = () => {
1106
+ return this.__store.state.matches.some((d) => d.notFoundError);
1107
+ };
1050
1108
  this.update({
1051
1109
  defaultPreloadDelay: 50,
1052
1110
  defaultPendingMs: 1e3,
@@ -1054,7 +1112,8 @@ class Router {
1054
1112
  context: void 0,
1055
1113
  ...options,
1056
1114
  stringifySearch: (options == null ? void 0 : options.stringifySearch) ?? defaultStringifySearch,
1057
- parseSearch: (options == null ? void 0 : options.parseSearch) ?? defaultParseSearch
1115
+ parseSearch: (options == null ? void 0 : options.parseSearch) ?? defaultParseSearch,
1116
+ transformer: (options == null ? void 0 : options.transformer) ?? JSON
1058
1117
  });
1059
1118
  }
1060
1119
  get state() {