@tanstack/react-router 1.6.1 → 1.7.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/react-router",
3
3
  "author": "Tanner Linsley",
4
- "version": "1.6.1",
4
+ "version": "1.7.1",
5
5
  "license": "MIT",
6
6
  "repository": "tanstack/router",
7
7
  "homepage": "https://tanstack.com/router",
@@ -44,7 +44,7 @@
44
44
  "@tanstack/store": "^0.1.3",
45
45
  "tiny-invariant": "^1.3.1",
46
46
  "tiny-warning": "^1.0.3",
47
- "@tanstack/history": "1.6.1"
47
+ "@tanstack/history": "1.7.1"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "rollup --config rollup.config.js"
package/src/Matches.tsx CHANGED
@@ -162,7 +162,13 @@ function MatchInner({
162
162
  })
163
163
 
164
164
  if (match.status === 'error') {
165
- throw match.error
165
+ if (isServerSideError(match.error)) {
166
+ const deserializeError =
167
+ router.options.errorSerializer?.deserialize ?? defaultDeserializeError
168
+ throw deserializeError(match.error.data)
169
+ } else {
170
+ throw match.error
171
+ }
166
172
  }
167
173
 
168
174
  if (match.status === 'pending') {
@@ -423,3 +429,24 @@ export function useLoaderData<
423
429
  },
424
430
  })
425
431
  }
432
+
433
+ export function isServerSideError(error: unknown): error is {
434
+ __isServerError: true
435
+ data: Record<string, any>
436
+ } {
437
+ if (!(typeof error === 'object' && error && 'data' in error)) return false
438
+ if (!('__isServerError' in error && error.__isServerError)) return false
439
+ if (!(typeof error.data === 'object' && error.data)) return false
440
+
441
+ return error.__isServerError === true
442
+ }
443
+
444
+ export function defaultDeserializeError(serializedData: Record<string, any>) {
445
+ if ('name' in serializedData && 'message' in serializedData) {
446
+ const error = new Error(serializedData.message)
447
+ error.name = serializedData.name
448
+ return error
449
+ }
450
+
451
+ return serializedData.data
452
+ }
package/src/awaited.tsx CHANGED
@@ -1,5 +1,8 @@
1
+ import warning from 'tiny-warning'
2
+ import { defaultDeserializeError, isServerSideError } from './Matches'
1
3
  import { useRouter } from './useRouter'
2
4
  import { DeferredPromise, isDehydratedDeferred } from './defer'
5
+ import { defaultSerializeError } from './router'
3
6
 
4
7
  export type AwaitOptions<T> = {
5
8
  promise: DeferredPromise<T>
@@ -13,6 +16,7 @@ export function useAwaited<T>({ promise }: AwaitOptions<T>): [T] {
13
16
 
14
17
  if (isDehydratedDeferred(promise)) {
15
18
  state = router.hydrateData(key)!
19
+ if (!state) throw new Error('Could not find dehydrated data')
16
20
  promise = Promise.resolve(state.data) as DeferredPromise<any>
17
21
  promise.__deferredState = state
18
22
  }
@@ -22,7 +26,27 @@ export function useAwaited<T>({ promise }: AwaitOptions<T>): [T] {
22
26
  }
23
27
 
24
28
  if (state.status === 'error') {
25
- throw state.error
29
+ if (typeof document !== 'undefined') {
30
+ if (isServerSideError(state.error)) {
31
+ throw (
32
+ router.options.errorSerializer?.deserialize ?? defaultDeserializeError
33
+ )(state.error.data as any)
34
+ } else {
35
+ warning(
36
+ false,
37
+ "Encountered a server-side error that doesn't fit the expected shape",
38
+ )
39
+ throw state.error
40
+ }
41
+ } else {
42
+ router.dehydrateData(key, state)
43
+ throw {
44
+ data: (
45
+ router.options.errorSerializer?.serialize ?? defaultSerializeError
46
+ )(state.error),
47
+ __isServerError: true,
48
+ }
49
+ }
26
50
  }
27
51
 
28
52
  router.dehydrateData(key, state)
package/src/defer.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { defaultSerializeError } from './router'
2
+
1
3
  export type DeferredPromiseState<T> = { uid: string } & (
2
4
  | {
3
5
  status: 'pending'
@@ -19,7 +21,12 @@ export type DeferredPromise<T> = Promise<T> & {
19
21
  __deferredState: DeferredPromiseState<T>
20
22
  }
21
23
 
22
- export function defer<T>(_promise: Promise<T>) {
24
+ export function defer<T>(
25
+ _promise: Promise<T>,
26
+ options?: {
27
+ serializeError?: typeof defaultSerializeError
28
+ },
29
+ ) {
23
30
  const promise = _promise as DeferredPromise<T>
24
31
 
25
32
  if (!promise.__deferredState) {
@@ -37,7 +44,10 @@ export function defer<T>(_promise: Promise<T>) {
37
44
  })
38
45
  .catch((error) => {
39
46
  state.status = 'error' as any
40
- state.error = error
47
+ state.error = {
48
+ data: (options?.serializeError ?? defaultSerializeError)(error),
49
+ __isServerError: true,
50
+ }
41
51
  })
42
52
  }
43
53
 
package/src/link.tsx CHANGED
@@ -51,7 +51,11 @@ export type Split<S, TIncludeTrailingSlash = true> = S extends unknown
51
51
  : never
52
52
 
53
53
  export type ParsePathParams<T extends string> = keyof {
54
- [K in Trim<Split<T>[number], '_'> as K extends `$${infer L}` ? L extends '' ? '_splat' : L: never]: K
54
+ [K in Trim<Split<T>[number], '_'> as K extends `$${infer L}`
55
+ ? L extends ''
56
+ ? '_splat'
57
+ : L
58
+ : never]: K
55
59
  }
56
60
 
57
61
  export type Join<T, Delimiter extends string = '/'> = T extends []
@@ -163,6 +167,16 @@ export type ToSubOptions<
163
167
  type ParamsReducer<TFrom, TTo> = TTo | ((current: TFrom) => TTo)
164
168
 
165
169
  type ParamVariant = 'PATH' | 'SEARCH'
170
+ type ExcludeRootSearchSchema<
171
+ T,
172
+ Excluded = Exclude<T, RootSearchSchema>,
173
+ > = [Excluded] extends [never] ? {} : Excluded
174
+
175
+ type PostProcessParams<
176
+ T,
177
+ TParamVariant extends ParamVariant,
178
+ > = TParamVariant extends 'SEARCH' ? ExcludeRootSearchSchema<T> : T
179
+
166
180
  export type ParamOptions<
167
181
  TRouteTree extends AnyRoute,
168
182
  TFrom,
@@ -179,11 +193,9 @@ export type ParamOptions<
179
193
  | 'fullSearchSchemaInput' = TParamVariant extends 'PATH'
180
194
  ? 'allParams'
181
195
  : 'fullSearchSchemaInput',
182
- TFromParams = Expand<
183
- Exclude<
184
- RouteByPath<TRouteTree, TFrom>['types'][TFromRouteType],
185
- RootSearchSchema
186
- >
196
+ TFromParams = PostProcessParams<
197
+ RouteByPath<TRouteTree, TFrom>['types'][TFromRouteType],
198
+ TParamVariant
187
199
  >,
188
200
  TToIndex = TTo extends ''
189
201
  ? ''
@@ -193,17 +205,13 @@ export type ParamOptions<
193
205
  TToParams = TToIndex extends ''
194
206
  ? TFromParams
195
207
  : never extends TResolved
196
- ? Expand<
197
- Exclude<
198
- RouteByPath<TRouteTree, TToIndex>['types'][TToRouteType],
199
- RootSearchSchema
200
- >
208
+ ? PostProcessParams<
209
+ RouteByPath<TRouteTree, TToIndex>['types'][TToRouteType],
210
+ TParamVariant
201
211
  >
202
- : Expand<
203
- Exclude<
204
- RouteByPath<TRouteTree, TResolved>['types'][TToRouteType],
205
- RootSearchSchema
206
- >
212
+ : PostProcessParams<
213
+ RouteByPath<TRouteTree, TResolved>['types'][TToRouteType],
214
+ TParamVariant
207
215
  >,
208
216
  TReducer = ParamsReducer<TFromParams, TToParams>,
209
217
  > = Expand<WithoutEmpty<PickRequired<TToParams>>> extends never
package/src/router.ts CHANGED
@@ -105,6 +105,7 @@ export type RouterContextOptions<TRouteTree extends AnyRoute> =
105
105
  export interface RouterOptions<
106
106
  TRouteTree extends AnyRoute,
107
107
  TDehydrated extends Record<string, any> = Record<string, any>,
108
+ TSerializedError extends Record<string, any> = Record<string, any>,
108
109
  > {
109
110
  history?: RouterHistory
110
111
  stringifySearch?: SearchSerializer
@@ -131,6 +132,11 @@ export interface RouterOptions<
131
132
  Wrap?: (props: { children: any }) => JSX.Element
132
133
  InnerWrap?: (props: { children: any }) => JSX.Element
133
134
  notFoundRoute?: AnyRoute
135
+ errorSerializer?: RouterErrorSerializer<TSerializedError>
136
+ }
137
+ export interface RouterErrorSerializer<TSerializedError> {
138
+ serialize: (err: unknown) => TSerializedError
139
+ deserialize: (err: TSerializedError) => unknown
134
140
  }
135
141
 
136
142
  export interface RouterState<TRouteTree extends AnyRoute = AnyRoute> {
@@ -180,7 +186,8 @@ export interface DehydratedRouter {
180
186
  export type RouterConstructorOptions<
181
187
  TRouteTree extends AnyRoute,
182
188
  TDehydrated extends Record<string, any>,
183
- > = Omit<RouterOptions<TRouteTree, TDehydrated>, 'context'> &
189
+ TSerializedError extends Record<string, any>,
190
+ > = Omit<RouterOptions<TRouteTree, TDehydrated, TSerializedError>, 'context'> &
184
191
  RouterContextOptions<TRouteTree>
185
192
 
186
193
  export const componentTypes = [
@@ -220,6 +227,7 @@ export type RouterListener<TRouterEvent extends RouterEvent> = {
220
227
  export class Router<
221
228
  TRouteTree extends AnyRoute = AnyRoute,
222
229
  TDehydrated extends Record<string, any> = Record<string, any>,
230
+ TSerializedError extends Record<string, any> = Record<string, any>,
223
231
  > {
224
232
  // Option-independent properties
225
233
  tempLocationKey: string | undefined = `${Math.round(
@@ -235,7 +243,7 @@ export class Router<
235
243
  // Must build in constructor
236
244
  __store!: Store<RouterState<TRouteTree>>
237
245
  options!: PickAsRequired<
238
- RouterOptions<TRouteTree, TDehydrated>,
246
+ RouterOptions<TRouteTree, TDehydrated, TSerializedError>,
239
247
  'stringifySearch' | 'parseSearch' | 'context'
240
248
  >
241
249
  history!: RouterHistory
@@ -246,7 +254,13 @@ export class Router<
246
254
  routesByPath!: RoutesByPath<TRouteTree>
247
255
  flatRoutes!: AnyRoute[]
248
256
 
249
- constructor(options: RouterConstructorOptions<TRouteTree, TDehydrated>) {
257
+ constructor(
258
+ options: RouterConstructorOptions<
259
+ TRouteTree,
260
+ TDehydrated,
261
+ TSerializedError
262
+ >,
263
+ ) {
250
264
  this.update({
251
265
  defaultPreloadDelay: 50,
252
266
  defaultPendingMs: 1000,
@@ -263,7 +277,13 @@ export class Router<
263
277
  // router can be used in a non-react environment if necessary
264
278
  startReactTransition: (fn: () => void) => void = (fn) => fn()
265
279
 
266
- update = (newOptions: RouterConstructorOptions<TRouteTree, TDehydrated>) => {
280
+ update = (
281
+ newOptions: RouterConstructorOptions<
282
+ TRouteTree,
283
+ TDehydrated,
284
+ TSerializedError
285
+ >,
286
+ ) => {
267
287
  const previousOptions = this.options
268
288
  this.options = {
269
289
  ...this.options,
@@ -1616,11 +1636,22 @@ export class Router<
1616
1636
  }
1617
1637
 
1618
1638
  dehydrate = (): DehydratedRouter => {
1639
+ const pickError =
1640
+ this.options.errorSerializer?.serialize ?? defaultSerializeError
1641
+
1619
1642
  return {
1620
1643
  state: {
1621
- dehydratedMatches: this.state.matches.map((d) =>
1622
- pick(d, ['id', 'status', 'updatedAt', 'loaderData']),
1623
- ),
1644
+ dehydratedMatches: this.state.matches.map((d) => ({
1645
+ ...pick(d, ['id', 'status', 'updatedAt', 'loaderData']),
1646
+ // If an error occurs server-side during SSRing,
1647
+ // send a small subset of the error to the client
1648
+ error: d.error
1649
+ ? {
1650
+ data: pickError(d.error),
1651
+ __isServerError: true,
1652
+ }
1653
+ : undefined,
1654
+ })),
1624
1655
  },
1625
1656
  }
1626
1657
  }
@@ -1713,3 +1744,15 @@ export function getInitialRouterState(
1713
1744
  lastUpdated: Date.now(),
1714
1745
  }
1715
1746
  }
1747
+
1748
+ export function defaultSerializeError(err: unknown) {
1749
+ if (err instanceof Error)
1750
+ return {
1751
+ name: err.name,
1752
+ message: err.message,
1753
+ }
1754
+
1755
+ return {
1756
+ data: err,
1757
+ }
1758
+ }