@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.
package/src/route.ts CHANGED
@@ -19,6 +19,8 @@ import {
19
19
  } from './utils'
20
20
  import { BuildLocationFn, NavigateFn } from './RouterProvider'
21
21
  import { LazyRoute } from '.'
22
+ import warning from 'tiny-warning'
23
+ import { NotFoundError, notFound } from '.'
22
24
 
23
25
  export const rootRouteId = '__root__' as const
24
26
  export type RootRouteId = typeof rootRouteId
@@ -191,6 +193,7 @@ export type UpdatableRouteOptions<
191
193
  // The content to be rendered when the route is matched. If no component is provided, defaults to `<Outlet />`
192
194
  component?: RouteComponent
193
195
  errorComponent?: false | null | ErrorRouteComponent
196
+ notFoundComponent?: NotFoundRouteComponent
194
197
  pendingComponent?: RouteComponent
195
198
  pendingMs?: number
196
199
  pendingMinMs?: number
@@ -368,12 +371,11 @@ export type MergeFromFromParent<T, U> = IsAny<T, U, T & U>
368
371
  export type ResolveAllParams<
369
372
  TParentRoute extends AnyRoute,
370
373
  TParams extends AnyPathParams,
371
- > =
372
- Record<never, string> extends TParentRoute['types']['allParams']
373
- ? TParams
374
- : Expand<
375
- UnionToIntersection<TParentRoute['types']['allParams'] & TParams> & {}
376
- >
374
+ > = Record<never, string> extends TParentRoute['types']['allParams']
375
+ ? TParams
376
+ : Expand<
377
+ UnionToIntersection<TParentRoute['types']['allParams'] & TParams> & {}
378
+ >
377
379
 
378
380
  export type RouteConstraints = {
379
381
  TParentRoute: AnyRoute
@@ -573,6 +575,10 @@ export class RouteApi<
573
575
  }): TSelected => {
574
576
  return useLoaderData({ ...opts, from: this.id } as any)
575
577
  }
578
+
579
+ notFound = (opts?: NotFoundError) => {
580
+ return notFound({ route: this.id as string, ...opts })
581
+ }
576
582
  }
577
583
 
578
584
  /**
@@ -1249,6 +1255,10 @@ export type ErrorComponentProps = {
1249
1255
  error: unknown
1250
1256
  info: { componentStack: string }
1251
1257
  }
1258
+ export type NotFoundRouteProps = {
1259
+ // TODO: Make sure this is `| null | undefined` (this is for global not-founds)
1260
+ data: unknown
1261
+ }
1252
1262
  //
1253
1263
 
1254
1264
  export type ReactNode = any
@@ -1266,6 +1276,8 @@ export type RouteComponent<TProps = any> = SyncRouteComponent<TProps> &
1266
1276
 
1267
1277
  export type ErrorRouteComponent = RouteComponent<ErrorComponentProps>
1268
1278
 
1279
+ export type NotFoundRouteComponent = SyncRouteComponent<NotFoundRouteProps>
1280
+
1269
1281
  export class NotFoundRoute<
1270
1282
  TParentRoute extends AnyRootRoute,
1271
1283
  TSearchSchemaInput extends Record<string, any> = {},
package/src/router.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  RouteMask,
18
18
  Route,
19
19
  LoaderFnContext,
20
+ rootRouteId,
20
21
  } from './route'
21
22
  import {
22
23
  FullSearchSchema,
@@ -63,6 +64,7 @@ import {
63
64
  } from './path'
64
65
  import invariant from 'tiny-invariant'
65
66
  import { isRedirect } from './redirects'
67
+ import { NotFoundError, isNotFound } from './not-found'
66
68
  import { ResolveRelativePath, ToOptions } from './link'
67
69
  import { NoInfer } from '@tanstack/react-store'
68
70
  // import warning from 'tiny-warning'
@@ -71,7 +73,7 @@ import { NoInfer } from '@tanstack/react-store'
71
73
 
72
74
  declare global {
73
75
  interface Window {
74
- __TSR_DEHYDRATED__?: HydrationCtx
76
+ __TSR_DEHYDRATED__?: { data: string }
75
77
  __TSR_ROUTER_CONTEXT__?: React.Context<Router<any>>
76
78
  }
77
79
  }
@@ -131,8 +133,20 @@ export interface RouterOptions<
131
133
  unmaskOnReload?: boolean
132
134
  Wrap?: (props: { children: any }) => JSX.Element
133
135
  InnerWrap?: (props: { children: any }) => JSX.Element
136
+ /**
137
+ * @deprecated
138
+ * Use `notFoundComponent` instead.
139
+ * See https://tanstack.com/router/v1/docs/guide/not-found-errors#migrating-from-notfoundroute for more info.
140
+ */
134
141
  notFoundRoute?: AnyRoute
142
+ transformer?: RouterTransformer
135
143
  errorSerializer?: RouterErrorSerializer<TSerializedError>
144
+ globalNotFound?: RouteComponent
145
+ }
146
+
147
+ export interface RouterTransformer {
148
+ stringify: (obj: unknown) => string
149
+ parse: (str: string) => unknown
136
150
  }
137
151
  export interface RouterErrorSerializer<TSerializedError> {
138
152
  serialize: (err: unknown) => TSerializedError
@@ -176,7 +190,7 @@ export interface DehydratedRouterState {
176
190
 
177
191
  export type DehydratedRouteMatch = Pick<
178
192
  RouteMatch,
179
- 'id' | 'status' | 'updatedAt' | 'loaderData'
193
+ 'id' | 'status' | 'updatedAt' | 'notFoundError' | 'loaderData'
180
194
  >
181
195
 
182
196
  export interface DehydratedRouter {
@@ -194,6 +208,7 @@ export const componentTypes = [
194
208
  'component',
195
209
  'errorComponent',
196
210
  'pendingComponent',
211
+ 'notFoundComponent',
197
212
  ] as const
198
213
 
199
214
  export type RouterEvents = {
@@ -256,7 +271,12 @@ export class Router<
256
271
  // Must build in constructor
257
272
  __store!: Store<RouterState<TRouteTree>>
258
273
  options!: PickAsRequired<
259
- RouterOptions<TRouteTree, TDehydrated, TSerializedError>,
274
+ Omit<
275
+ RouterOptions<TRouteTree, TDehydrated, TSerializedError>,
276
+ 'transformer'
277
+ > & {
278
+ transformer: RouterTransformer
279
+ },
260
280
  'stringifySearch' | 'parseSearch' | 'context'
261
281
  >
262
282
  history!: RouterHistory
@@ -282,6 +302,7 @@ export class Router<
282
302
  ...options,
283
303
  stringifySearch: options?.stringifySearch ?? defaultStringifySearch,
284
304
  parseSearch: options?.parseSearch ?? defaultParseSearch,
305
+ transformer: options?.transformer ?? JSON,
285
306
  })
286
307
  }
287
308
 
@@ -297,6 +318,12 @@ export class Router<
297
318
  TSerializedError
298
319
  >,
299
320
  ) => {
321
+ if (newOptions.notFoundRoute) {
322
+ console.warn(
323
+ '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.',
324
+ )
325
+ }
326
+
300
327
  const previousOptions = this.options
301
328
  this.options = {
302
329
  ...this.options,
@@ -581,17 +608,23 @@ export class Router<
581
608
 
582
609
  let matchedRoutes: AnyRoute[] = [routeCursor]
583
610
 
611
+ let isGlobalNotFound = false
612
+
584
613
  // Check to see if the route needs a 404 entry
585
614
  if (
586
615
  // If we found a route, and it's not an index route and we have left over path
587
- (foundRoute
616
+ foundRoute
588
617
  ? foundRoute.path !== '/' && routeParams['**']
589
618
  : // Or if we didn't find a route and we have left over path
590
- trimPathRight(pathname)) &&
591
- // And we have a 404 route configured
592
- this.options.notFoundRoute
619
+ trimPathRight(pathname)
593
620
  ) {
594
- matchedRoutes.push(this.options.notFoundRoute)
621
+ // If the user has defined an (old) 404 route, use it
622
+ if (this.options.notFoundRoute) {
623
+ matchedRoutes.push(this.options.notFoundRoute)
624
+ } else {
625
+ // If there is no routes found during path matching
626
+ isGlobalNotFound = true
627
+ }
595
628
  }
596
629
 
597
630
  while (routeCursor?.parentRoute) {
@@ -710,7 +743,14 @@ export class Router<
710
743
  )
711
744
 
712
745
  const match: AnyRouteMatch = existingMatch
713
- ? { ...existingMatch, cause }
746
+ ? {
747
+ ...existingMatch,
748
+ cause,
749
+ notFoundError:
750
+ isGlobalNotFound && route.id === rootRouteId
751
+ ? { global: true }
752
+ : undefined,
753
+ }
714
754
  : {
715
755
  id: matchId,
716
756
  routeId: route.id,
@@ -733,6 +773,10 @@ export class Router<
733
773
  loaderDeps,
734
774
  invalid: false,
735
775
  preload: false,
776
+ notFoundError:
777
+ isGlobalNotFound && route.id === rootRouteId
778
+ ? { global: true }
779
+ : undefined,
736
780
  links: route.options.links?.(),
737
781
  scripts: route.options.scripts?.(),
738
782
  staticData: route.options.staticData || {},
@@ -781,8 +825,8 @@ export class Router<
781
825
  this.latestLocation.pathname,
782
826
  fromSearch,
783
827
  )
784
- const stayingMatches = matches?.filter((d) =>
785
- fromMatches?.find((e) => e.routeId === d.routeId),
828
+ const stayingMatches = matches?.filter(
829
+ (d) => fromMatches?.find((e) => e.routeId === d.routeId),
786
830
  )
787
831
 
788
832
  const prevParams = { ...last(fromMatches)?.params }
@@ -1098,6 +1142,10 @@ export class Router<
1098
1142
  throw err
1099
1143
  }
1100
1144
 
1145
+ if (isNotFound(err)) {
1146
+ this.updateMatchesWithNotFound(matches, match, err)
1147
+ }
1148
+
1101
1149
  try {
1102
1150
  route.options.onError?.(err)
1103
1151
  } catch (errorHandlerErr) {
@@ -1200,6 +1248,11 @@ export class Router<
1200
1248
  }
1201
1249
  return true
1202
1250
  }
1251
+
1252
+ if (isNotFound(err)) {
1253
+ this.updateMatchesWithNotFound(matches, match, err)
1254
+ }
1255
+
1203
1256
  return false
1204
1257
  }
1205
1258
 
@@ -1663,7 +1716,7 @@ export class Router<
1663
1716
  typeof getData === 'function' ? await (getData as any)() : getData
1664
1717
  return `<script id='${id}' suppressHydrationWarning>window["__TSR_DEHYDRATED__${escapeJSON(
1665
1718
  strKey,
1666
- )}"] = ${JSON.stringify(data)}
1719
+ )}"] = ${JSON.stringify(this.options.transformer.stringify(data))}
1667
1720
  ;(() => {
1668
1721
  var el = document.getElementById('${id}')
1669
1722
  el.parentElement.removeChild(el)
@@ -1681,7 +1734,9 @@ export class Router<
1681
1734
  if (typeof document !== 'undefined') {
1682
1735
  const strKey = typeof key === 'string' ? key : JSON.stringify(key)
1683
1736
 
1684
- return window[`__TSR_DEHYDRATED__${strKey}` as any] as T
1737
+ return this.options.transformer.parse(
1738
+ window[`__TSR_DEHYDRATED__${strKey}` as any] as unknown as string,
1739
+ ) as T
1685
1740
  }
1686
1741
 
1687
1742
  return undefined
@@ -1694,7 +1749,15 @@ export class Router<
1694
1749
  return {
1695
1750
  state: {
1696
1751
  dehydratedMatches: this.state.matches.map((d) => ({
1697
- ...pick(d, ['id', 'status', 'updatedAt', 'loaderData']),
1752
+ ...pick(d, [
1753
+ 'id',
1754
+ 'status',
1755
+ 'updatedAt',
1756
+ 'loaderData',
1757
+ // Not-founds that occur during SSR don't require the client to load data before
1758
+ // triggering in order to prevent the flicker of the loading component
1759
+ 'notFoundError',
1760
+ ]),
1698
1761
  // If an error occurs server-side during SSRing,
1699
1762
  // send a small subset of the error to the client
1700
1763
  error: d.error
@@ -1708,11 +1771,11 @@ export class Router<
1708
1771
  }
1709
1772
  }
1710
1773
 
1711
- hydrate = async (__do_not_use_server_ctx?: HydrationCtx) => {
1774
+ hydrate = async (__do_not_use_server_ctx?: string) => {
1712
1775
  let _ctx = __do_not_use_server_ctx
1713
1776
  // Client hydrates from window
1714
1777
  if (typeof document !== 'undefined') {
1715
- _ctx = window.__TSR_DEHYDRATED__
1778
+ _ctx = window.__TSR_DEHYDRATED__?.data
1716
1779
  }
1717
1780
 
1718
1781
  invariant(
@@ -1720,7 +1783,7 @@ export class Router<
1720
1783
  'Expected to find a __TSR_DEHYDRATED__ property on window... but we did not. Did you forget to render <DehydrateRouter /> in your app?',
1721
1784
  )
1722
1785
 
1723
- const ctx = _ctx
1786
+ const ctx = this.options.transformer.parse(_ctx) as HydrationCtx
1724
1787
  this.dehydratedData = ctx.payload as any
1725
1788
  this.options.hydrate?.(ctx.payload as any)
1726
1789
  const dehydratedState = ctx.router.state
@@ -1763,6 +1826,46 @@ export class Router<
1763
1826
  })
1764
1827
  }
1765
1828
 
1829
+ // Finds a match that has a notFoundComponent
1830
+ updateMatchesWithNotFound = (
1831
+ matches: AnyRouteMatch[],
1832
+ currentMatch: AnyRouteMatch,
1833
+ err: NotFoundError,
1834
+ ) => {
1835
+ const matchesByRouteId = Object.fromEntries(
1836
+ matches.map((match) => [match.routeId, match]),
1837
+ ) as Record<string, AnyRouteMatch>
1838
+
1839
+ if (err.global) {
1840
+ matchesByRouteId[rootRouteId]!.notFoundError = err
1841
+ } else {
1842
+ // If the err contains a routeId, start searching up from that route
1843
+ let currentRoute = (this.routesById as any)[
1844
+ err.route ?? currentMatch.routeId
1845
+ ] as AnyRoute
1846
+
1847
+ // Go up the tree until we find a route with a notFoundComponent
1848
+ while (!currentRoute.options.notFoundComponent) {
1849
+ currentRoute = currentRoute?.parentRoute
1850
+
1851
+ invariant(
1852
+ currentRoute,
1853
+ 'Found invalid route tree while trying to find not-found handler.',
1854
+ )
1855
+
1856
+ if (currentRoute.id === rootRouteId) break
1857
+ }
1858
+
1859
+ const match = matchesByRouteId[currentRoute.id]
1860
+ invariant(match, 'Could not find match for route: ' + currentRoute.id)
1861
+ match.notFoundError = err
1862
+ }
1863
+ }
1864
+
1865
+ hasNotFoundMatch = () => {
1866
+ return this.__store.state.matches.some((d) => d.notFoundError)
1867
+ }
1868
+
1766
1869
  // resolveMatchPromise = (matchId: string, key: string, value: any) => {
1767
1870
  // state.matches
1768
1871
  // .find((d) => d.id === matchId)