@tanstack/react-router 0.0.1-beta.214 → 0.0.1-beta.216

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": "0.0.1-beta.214",
4
+ "version": "0.0.1-beta.216",
5
5
  "license": "MIT",
6
6
  "repository": "tanstack/router",
7
7
  "homepage": "https://tanstack.com/router",
@@ -42,7 +42,7 @@
42
42
  "@babel/runtime": "^7.16.7",
43
43
  "tiny-invariant": "^1.3.1",
44
44
  "tiny-warning": "^1.0.3",
45
- "@tanstack/history": "0.0.1-beta.214"
45
+ "@tanstack/history": "0.0.1-beta.216"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "rollup --config rollup.config.js"
package/src/Matches.tsx CHANGED
@@ -8,7 +8,7 @@ import { ResolveRelativePath, ToOptions } from './link'
8
8
  import { AnyRoute, ReactNode, rootRouteId } from './route'
9
9
  import { RouteById, RouteByPath, RouteIds, RoutePaths } from './routeInfo'
10
10
  import { RegisteredRouter } from './router'
11
- import { NoInfer, StrictOrFrom } from './utils'
11
+ import { NoInfer, StrictOrFrom, functionalUpdate } from './utils'
12
12
 
13
13
  export function Matches() {
14
14
  const { routesById, state } = useRouter()
@@ -16,7 +16,7 @@ export function Matches() {
16
16
 
17
17
  const locationKey = useRouterState().location.state.key
18
18
 
19
- const route = routesById[rootRouteId]
19
+ const route = routesById[rootRouteId]!
20
20
 
21
21
  const errorComponent = React.useCallback(
22
22
  (props: any) => {
@@ -58,7 +58,7 @@ export function Match({ matches }: { matches: RouteMatch[] }) {
58
58
  const { options, routesById } = useRouter()
59
59
  const match = matches[0]!
60
60
  const routeId = match?.routeId
61
- const route = routesById[routeId]
61
+ const route = routesById[routeId]!
62
62
  const locationKey = useRouterState().location.state?.key
63
63
 
64
64
  const PendingComponent = (route.options.pendingComponent ??
@@ -70,9 +70,9 @@ export function Match({ matches }: { matches: RouteMatch[] }) {
70
70
  options.defaultErrorComponent ??
71
71
  ErrorComponent
72
72
 
73
- const ResolvedSuspenseBoundary =
74
- route.options.wrapInSuspense ?? React.Suspense
75
- // const ResolvedSuspenseBoundary = SafeFragment
73
+ const ResolvedSuspenseBoundary = route.options.wrapInSuspense
74
+ ? React.Suspense
75
+ : SafeFragment
76
76
 
77
77
  const errorComponent = React.useCallback(
78
78
  (props: any) => {
@@ -112,7 +112,7 @@ export function Match({ matches }: { matches: RouteMatch[] }) {
112
112
  }
113
113
  function MatchInner({ match }: { match: RouteMatch }): any {
114
114
  const { options, routesById } = useRouter()
115
- const route = routesById[match.routeId]
115
+ const route = routesById[match.routeId]!
116
116
 
117
117
  if (match.status === 'error') {
118
118
  throw match.error
@@ -131,7 +131,8 @@ function MatchInner({ match }: { match: RouteMatch }): any {
131
131
  useRouteContext: route.useRouteContext as any,
132
132
  useSearch: route.useSearch,
133
133
  useParams: route.useParams as any,
134
- } as any)
134
+ useLoaderData: route.useLoaderData,
135
+ })
135
136
  }
136
137
 
137
138
  return <Outlet />
@@ -248,7 +249,7 @@ export function useMatch<
248
249
  opts: StrictOrFrom<TFrom> & {
249
250
  select?: (match: TRouteMatchState) => TSelected
250
251
  },
251
- ): TStrict extends true ? TRouteMatchState : TRouteMatchState | undefined {
252
+ ): TStrict extends true ? TSelected : TSelected | undefined {
252
253
  const nearestMatch = React.useContext(matchesContext)[0]!
253
254
  const nearestMatchRouteId = nearestMatch?.routeId
254
255
 
@@ -313,3 +314,24 @@ export function useMatches<T = RouteMatch[]>(opts?: {
313
314
  },
314
315
  })
315
316
  }
317
+
318
+ export function useLoaderData<
319
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
320
+ TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
321
+ TStrict extends boolean = true,
322
+ TRouteMatch extends RouteMatch<TRouteTree, TFrom> = RouteMatch<
323
+ TRouteTree,
324
+ TFrom
325
+ >,
326
+ TSelected = TRouteMatch['loaderData'],
327
+ >(
328
+ opts: StrictOrFrom<TFrom> & {
329
+ select?: (match: TRouteMatch) => TSelected
330
+ },
331
+ ): TStrict extends true ? TSelected : TSelected | undefined {
332
+ const match = useMatch({ ...opts, select: undefined })!
333
+
334
+ return typeof opts.select === 'function'
335
+ ? opts.select(match?.loaderData)
336
+ : match?.loaderData
337
+ }
@@ -27,7 +27,13 @@ import {
27
27
  trimPathRight,
28
28
  } from './path'
29
29
  import { isRedirect } from './redirects'
30
- import { AnyPathParams, AnyRoute, AnySearchSchema, Route } from './route'
30
+ import {
31
+ AnyPathParams,
32
+ AnyRoute,
33
+ AnySearchSchema,
34
+ LoaderFnContext,
35
+ Route,
36
+ } from './route'
31
37
  import {
32
38
  FullSearchSchema,
33
39
  ParseRoute,
@@ -83,16 +89,13 @@ export type BuildLinkFn<TRouteTree extends AnyRoute> = <
83
89
  ) => LinkInfo
84
90
 
85
91
  export type NavigateFn<TRouteTree extends AnyRoute> = <
86
- TRouteTree extends AnyRoute,
87
92
  TFrom extends RoutePaths<TRouteTree> = '/',
88
93
  TTo extends string = '',
89
94
  TMaskFrom extends RoutePaths<TRouteTree> = TFrom,
90
95
  TMaskTo extends string = '',
91
- >({
92
- from,
93
- to = '' as any,
94
- ...rest
95
- }: NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>) => Promise<void>
96
+ >(
97
+ opts: NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>,
98
+ ) => Promise<void>
96
99
 
97
100
  export type MatchRouteFn<TRouteTree extends AnyRoute> = <
98
101
  TFrom extends RoutePaths<TRouteTree> = '/',
@@ -250,12 +253,14 @@ export function RouterProvider<
250
253
  getInitialRouterState(latestLocationRef.current),
251
254
  )
252
255
  const [isTransitioning, startReactTransition] = React.useTransition()
256
+ const pendingMatchesRef = React.useRef<AnyRouteMatch[]>([])
253
257
 
254
258
  const state = React.useMemo<RouterState<TRouteTree>>(
255
259
  () => ({
256
260
  ...preState,
257
261
  status: isTransitioning ? 'pending' : 'idle',
258
262
  location: isTransitioning ? latestLocationRef.current : preState.location,
263
+ pendingMatches: pendingMatchesRef.current,
259
264
  }),
260
265
  [preState, isTransitioning],
261
266
  )
@@ -268,6 +273,8 @@ export function RouterProvider<
268
273
  toLocation: state.location,
269
274
  pathChanged: state.location!.href !== state.resolvedLocation?.href,
270
275
  })
276
+ pendingMatchesRef.current = []
277
+
271
278
  setState((s) => ({
272
279
  ...s,
273
280
  resolvedLocation: s.location,
@@ -464,7 +471,7 @@ export function RouterProvider<
464
471
 
465
472
  // Create a fresh route match
466
473
  const hasLoaders = !!(
467
- route.options.load ||
474
+ route.options.loader ||
468
475
  componentTypes.some((d) => (route.options[d] as any)?.preload)
469
476
  )
470
477
 
@@ -938,10 +945,6 @@ export function RouterProvider<
938
945
  const parentMatchPromise = matchPromises[index - 1]
939
946
  const route = looseRoutesById[match.routeId]!
940
947
 
941
- if (match.isFetching) {
942
- return getRouteMatch(state, match.id)?.loadPromise
943
- }
944
-
945
948
  const handleIfRedirect = (err: any) => {
946
949
  if (isRedirect(err)) {
947
950
  if (!preload) {
@@ -952,8 +955,50 @@ export function RouterProvider<
952
955
  return false
953
956
  }
954
957
 
955
- const load = async () => {
956
- try {
958
+ let loadPromise: Promise<void> | undefined
959
+
960
+ matches[index] = match = {
961
+ ...match,
962
+ fetchedAt: Date.now(),
963
+ invalid: false,
964
+ }
965
+
966
+ if (match.isFetching) {
967
+ loadPromise = getRouteMatch(state, match.id)?.loadPromise
968
+ } else {
969
+ const cause = state.matches.find((d) => d.id === match.id)
970
+ ? 'stay'
971
+ : 'enter'
972
+
973
+ const loaderContext: LoaderFnContext = {
974
+ params: match.params,
975
+ search: match.search,
976
+ preload: !!preload,
977
+ parentMatchPromise,
978
+ abortController: match.abortController,
979
+ context: match.context,
980
+ location: state.location,
981
+ navigate: (opts) =>
982
+ navigate({ ...opts, from: match.pathname } as any),
983
+ cause,
984
+ }
985
+
986
+ // Default to reloading the route all the time
987
+ const shouldReload =
988
+ route.options.shouldReload?.(loaderContext) ?? true
989
+
990
+ // If the user doesn't want the route to reload, just
991
+ // resolve with the existing loader data
992
+
993
+ if (!shouldReload) {
994
+ loadPromise = Promise.resolve(match.loaderData)
995
+ } else {
996
+ // Otherwise, load the route
997
+ matches[index] = match = {
998
+ ...match,
999
+ isFetching: true,
1000
+ }
1001
+
957
1002
  const componentsPromise = Promise.all(
958
1003
  componentTypes.map(async (type) => {
959
1004
  const component = route.options[type]
@@ -964,78 +1009,66 @@ export function RouterProvider<
964
1009
  }),
965
1010
  )
966
1011
 
967
- const loaderPromise = route.options.load?.({
968
- params: match.params,
969
- search: match.search,
970
- preload: !!preload,
971
- parentMatchPromise,
972
- abortController: match.abortController,
973
- context: match.context,
974
- location: state.location,
975
- navigate: (opts) =>
976
- navigate({ ...opts, from: match.pathname }),
977
- })
978
-
979
- const [_, loaderContext] = await Promise.all([
1012
+ const loaderPromise = route.options.loader?.(loaderContext)
1013
+
1014
+ loadPromise = Promise.all([
980
1015
  componentsPromise,
981
1016
  loaderPromise,
982
- ])
983
- if ((latestPromise = checkLatest())) return await latestPromise
984
-
985
- matches[index] = match = {
986
- ...match,
987
- error: undefined,
988
- status: 'success',
989
- isFetching: false,
990
- updatedAt: Date.now(),
991
- }
992
- } catch (error) {
993
- if ((latestPromise = checkLatest())) return await latestPromise
994
- if (handleIfRedirect(error)) return
995
-
996
- try {
997
- route.options.onError?.(error)
998
- } catch (onErrorError) {
999
- error = onErrorError
1000
- if (handleIfRedirect(onErrorError)) return
1001
- }
1002
-
1003
- matches[index] = match = {
1004
- ...match,
1005
- error,
1006
- status: 'error',
1007
- isFetching: false,
1008
- updatedAt: Date.now(),
1009
- }
1010
- }
1011
-
1012
- if (!preload) {
1013
- setState((s) => ({
1014
- ...s,
1015
- matches: s.matches.map((d) =>
1016
- d.id === match.id ? match : d,
1017
- ),
1018
- }))
1017
+ ]).then((d) => d[1])
1019
1018
  }
1020
1019
  }
1021
1020
 
1022
- let loadPromise: Promise<void> | undefined
1023
-
1024
1021
  matches[index] = match = {
1025
1022
  ...match,
1026
- isFetching: true,
1027
- fetchedAt: Date.now(),
1028
- invalid: false,
1023
+ loadPromise,
1029
1024
  }
1030
1025
 
1031
- loadPromise = load()
1026
+ if (!preload) {
1027
+ setState((s) => ({
1028
+ ...s,
1029
+ matches: s.matches.map((d) => (d.id === match.id ? match : d)),
1030
+ }))
1031
+ }
1032
1032
 
1033
- matches[index] = match = {
1034
- ...match,
1035
- loadPromise,
1033
+ try {
1034
+ const loaderData = await loadPromise
1035
+ if ((latestPromise = checkLatest())) return await latestPromise
1036
+
1037
+ matches[index] = match = {
1038
+ ...match,
1039
+ error: undefined,
1040
+ status: 'success',
1041
+ isFetching: false,
1042
+ updatedAt: Date.now(),
1043
+ loaderData,
1044
+ loadPromise: undefined,
1045
+ }
1046
+ } catch (error) {
1047
+ if ((latestPromise = checkLatest())) return await latestPromise
1048
+ if (handleIfRedirect(error)) return
1049
+
1050
+ try {
1051
+ route.options.onError?.(error)
1052
+ } catch (onErrorError) {
1053
+ error = onErrorError
1054
+ if (handleIfRedirect(onErrorError)) return
1055
+ }
1056
+
1057
+ matches[index] = match = {
1058
+ ...match,
1059
+ error,
1060
+ status: 'error',
1061
+ isFetching: false,
1062
+ updatedAt: Date.now(),
1063
+ }
1036
1064
  }
1037
1065
 
1038
- await loadPromise
1066
+ if (!preload) {
1067
+ setState((s) => ({
1068
+ ...s,
1069
+ matches: s.matches.map((d) => (d.id === match.id ? match : d)),
1070
+ }))
1071
+ }
1039
1072
  })(),
1040
1073
  )
1041
1074
  })
@@ -1071,6 +1104,8 @@ export function RouterProvider<
1071
1104
  },
1072
1105
  )
1073
1106
 
1107
+ pendingMatchesRef.current = matches
1108
+
1074
1109
  const previousMatches = state.matches
1075
1110
 
1076
1111
  // Ingest the new matches
@@ -1099,13 +1134,13 @@ export function RouterProvider<
1099
1134
  }
1100
1135
 
1101
1136
  const exitingMatchIds = previousMatches.filter(
1102
- (id) => !state.pendingMatches.includes(id),
1137
+ (id) => !pendingMatchesRef.current.includes(id),
1103
1138
  )
1104
- const enteringMatchIds = state.pendingMatches.filter(
1139
+ const enteringMatchIds = pendingMatchesRef.current.filter(
1105
1140
  (id) => !previousMatches.includes(id),
1106
1141
  )
1107
1142
  const stayingMatchIds = previousMatches.filter((id) =>
1108
- state.pendingMatches.includes(id),
1143
+ pendingMatchesRef.current.includes(id),
1109
1144
  )
1110
1145
 
1111
1146
  // setState((s) => ({
@@ -1326,10 +1361,7 @@ export function RouterProvider<
1326
1361
  }
1327
1362
  }, [history])
1328
1363
 
1329
- const initialLoad = React.useRef(true)
1330
-
1331
- if (initialLoad.current) {
1332
- initialLoad.current = false
1364
+ React.useLayoutEffect(() => {
1333
1365
  startReactTransition(() => {
1334
1366
  try {
1335
1367
  load()
@@ -1337,7 +1369,7 @@ export function RouterProvider<
1337
1369
  console.error(err)
1338
1370
  }
1339
1371
  })
1340
- }
1372
+ }, [])
1341
1373
 
1342
1374
  const matchRoute = useStableCallback<MatchRouteFn<TRouteTree>>(
1343
1375
  (location, opts) => {
@@ -1454,6 +1486,7 @@ export interface RouteMatch<
1454
1486
  searchError: unknown
1455
1487
  updatedAt: number
1456
1488
  loadPromise?: Promise<void>
1489
+ loaderData?: RouteById<TRouteTree, TRouteId>['types']['loaderData']
1457
1490
  __resolveLoadPromise?: () => void
1458
1491
  context: RouteById<TRouteTree, TRouteId>['types']['allContext']
1459
1492
  routeSearch: RouteById<TRouteTree, TRouteId>['types']['searchSchema']
package/src/fileRoute.ts CHANGED
@@ -104,6 +104,7 @@ export class FileRoute<
104
104
  Assign<IsAny<TParentRoute['types']['allContext'], {}>, TRouteContext>
105
105
  >,
106
106
  TRouterContext extends RouteConstraints['TRouterContext'] = AnyContext,
107
+ TLoaderData extends any = unknown,
107
108
  TChildren extends RouteConstraints['TChildren'] = unknown,
108
109
  TRouteTree extends RouteConstraints['TRouteTree'] = AnyRoute,
109
110
  >(
@@ -117,11 +118,17 @@ export class FileRoute<
117
118
  TParams,
118
119
  TAllParams,
119
120
  TRouteContext,
120
- TContext
121
+ TContext,
122
+ TLoaderData
121
123
  >,
122
124
  'getParentRoute' | 'path' | 'id'
123
125
  > &
124
- UpdatableRouteOptions<TFullSearchSchema, TAllParams, TContext>,
126
+ UpdatableRouteOptions<
127
+ TFullSearchSchema,
128
+ TAllParams,
129
+ TContext,
130
+ TLoaderData
131
+ >,
125
132
  ): Route<
126
133
  TParentRoute,
127
134
  TPath,
@@ -135,6 +142,7 @@ export class FileRoute<
135
142
  TRouteContext,
136
143
  TContext,
137
144
  TRouterContext,
145
+ TLoaderData,
138
146
  TChildren,
139
147
  TRouteTree
140
148
  > => {