@tanstack/react-router 0.0.1-beta.272 → 0.0.1-beta.274

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.272",
4
+ "version": "0.0.1-beta.274",
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": "0.0.1-beta.272"
47
+ "@tanstack/history": "0.0.1-beta.274"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "rollup --config rollup.config.js"
package/src/Matches.tsx CHANGED
@@ -39,10 +39,10 @@ export interface RouteMatch<
39
39
  context: RouteById<TRouteTree, TRouteId>['types']['allContext']
40
40
  search: FullSearchSchema<TRouteTree> &
41
41
  RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema']
42
- fetchedAt: number
42
+ fetchCount: number
43
43
  shouldReloadDeps: any
44
44
  abortController: AbortController
45
- cause: 'enter' | 'stay'
45
+ cause: 'preload' | 'enter' | 'stay'
46
46
  }
47
47
 
48
48
  export type AnyRouteMatch = RouteMatch<any>
@@ -54,10 +54,14 @@ export type BuildLocationFn<TRouteTree extends AnyRoute> = (
54
54
 
55
55
  export type InjectedHtmlEntry = string | (() => Promise<string> | string)
56
56
 
57
- export const routerContext = React.createContext<Router<any>>(null!)
57
+ export let routerContext = React.createContext<Router<any>>(null!)
58
58
 
59
59
  if (typeof document !== 'undefined') {
60
- window.__TSR_ROUTER_CONTEXT__ = routerContext as any
60
+ if (window.__TSR_ROUTER_CONTEXT__) {
61
+ routerContext = window.__TSR_ROUTER_CONTEXT__
62
+ } else {
63
+ window.__TSR_ROUTER_CONTEXT__ = routerContext as any
64
+ }
61
65
  }
62
66
 
63
67
  export function RouterProvider<
@@ -174,11 +178,13 @@ function Transitioner() {
174
178
  })
175
179
 
176
180
  if ((document as any).querySelector) {
177
- const el = document.getElementById(
178
- routerState.location.hash,
179
- ) as HTMLElement | null
180
- if (el) {
181
- el.scrollIntoView()
181
+ if (routerState.location.hash !== '') {
182
+ const el = document.getElementById(
183
+ routerState.location.hash,
184
+ ) as HTMLElement | null
185
+ if (el) {
186
+ el.scrollIntoView()
187
+ }
182
188
  }
183
189
  }
184
190
  router.pendingMatches = []
@@ -210,9 +216,11 @@ export function getRouteMatch<TRouteTree extends AnyRoute>(
210
216
  state: RouterState<TRouteTree>,
211
217
  id: string,
212
218
  ): undefined | RouteMatch<TRouteTree> {
213
- return [...(state.pendingMatches ?? []), ...state.matches].find(
214
- (d) => d.id === id,
215
- )
219
+ return [
220
+ ...state.preloadMatches,
221
+ ...(state.pendingMatches ?? []),
222
+ ...state.matches,
223
+ ].find((d) => d.id === id)
216
224
  }
217
225
 
218
226
  export function useRouterState<
package/src/route.ts CHANGED
@@ -171,7 +171,7 @@ type BeforeLoadFn<
171
171
  location: ParsedLocation
172
172
  navigate: NavigateFn<AnyRoute>
173
173
  buildLocation: BuildLocationFn<AnyRoute>
174
- cause: 'enter' | 'stay'
174
+ cause: 'preload' | 'enter' | 'stay'
175
175
  }) => Promise<TRouteContext> | TRouteContext | void
176
176
 
177
177
  export type UpdatableRouteOptions<
@@ -268,7 +268,7 @@ export interface LoaderFnContext<
268
268
  location: ParsedLocation<TFullSearchSchema>
269
269
  navigate: (opts: NavigateOptions<AnyRoute>) => Promise<void>
270
270
  parentMatchPromise?: Promise<void>
271
- cause: 'enter' | 'stay'
271
+ cause: 'preload' | 'enter' | 'stay'
272
272
  }
273
273
 
274
274
  export type SearchFilter<T, U = T> = (prev: T) => U
package/src/router.ts CHANGED
@@ -110,6 +110,7 @@ export interface RouterOptions<
110
110
  defaultPendingComponent?: RouteComponent
111
111
  defaultPendingMs?: number
112
112
  defaultPendingMinMs?: number
113
+ defaultPreloadMaxAge?: number
113
114
  caseSensitive?: boolean
114
115
  routeTree?: TRouteTree
115
116
  basepath?: string
@@ -129,6 +130,7 @@ export interface RouterState<TRouteTree extends AnyRoute = AnyRoute> {
129
130
  isTransitioning: boolean
130
131
  matches: RouteMatch<TRouteTree>[]
131
132
  pendingMatches?: RouteMatch<TRouteTree>[]
133
+ preloadMatches: RouteMatch<TRouteTree>[]
132
134
  location: ParsedLocation<FullSearchSchema<TRouteTree>>
133
135
  resolvedLocation: ParsedLocation<FullSearchSchema<TRouteTree>>
134
136
  lastUpdated: number
@@ -159,7 +161,7 @@ export interface DehydratedRouterState {
159
161
 
160
162
  export type DehydratedRouteMatch = Pick<
161
163
  RouteMatch,
162
- 'fetchedAt' | 'id' | 'status' | 'updatedAt'
164
+ 'id' | 'status' | 'updatedAt'
163
165
  >
164
166
 
165
167
  export interface DehydratedRouter {
@@ -658,7 +660,7 @@ export class Router<
658
660
  context: undefined!,
659
661
  abortController: new AbortController(),
660
662
  shouldReloadDeps: undefined,
661
- fetchedAt: 0,
663
+ fetchCount: 0,
662
664
  cause,
663
665
  }
664
666
 
@@ -976,10 +978,20 @@ export class Router<
976
978
  let latestPromise
977
979
  let firstBadMatchIndex: number | undefined
978
980
 
979
- const updatePendingMatch = (match: AnyRouteMatch) => {
981
+ const updateMatch = (match: AnyRouteMatch) => {
982
+ const isPreload = this.state.preloadMatches.find((d) => d.id === match.id)
983
+ const isPending = this.state.pendingMatches?.find(
984
+ (d) => d.id === match.id,
985
+ )
986
+ const matchesKey = isPreload
987
+ ? 'preloadMatches'
988
+ : isPending
989
+ ? 'pendingMatches'
990
+ : 'matches'
991
+
980
992
  this.__store.setState((s) => ({
981
993
  ...s,
982
- pendingMatches: s.pendingMatches?.map((d) =>
994
+ [matchesKey]: s[matchesKey]?.map((d) =>
983
995
  d.id === match.id ? match : d,
984
996
  ),
985
997
  }))
@@ -1043,7 +1055,7 @@ export class Router<
1043
1055
  navigate: (opts) =>
1044
1056
  this.navigate({ ...opts, from: match.pathname } as any),
1045
1057
  buildLocation: this.buildLocation,
1046
- cause: match.cause,
1058
+ cause: preload ? 'preload' : match.cause,
1047
1059
  })) ?? ({} as any)
1048
1060
 
1049
1061
  if (isRedirect(beforeLoadContext)) {
@@ -1083,7 +1095,7 @@ export class Router<
1083
1095
 
1084
1096
  validResolvedMatches.forEach((match, index) => {
1085
1097
  matchPromises.push(
1086
- (async () => {
1098
+ new Promise<void>(async (resolve) => {
1087
1099
  const parentMatchPromise = matchPromises[index - 1]
1088
1100
  const route = this.looseRoutesById[match.routeId]!
1089
1101
 
@@ -1101,126 +1113,129 @@ export class Router<
1101
1113
 
1102
1114
  matches[index] = match = {
1103
1115
  ...match,
1104
- fetchedAt: Date.now(),
1105
1116
  showPending: false,
1106
1117
  }
1107
1118
 
1119
+ let didShowPending = false
1108
1120
  const pendingMs =
1109
1121
  route.options.pendingMs ?? this.options.defaultPendingMs
1110
-
1111
- let pendingPromise: Promise<void> | undefined
1112
-
1113
- if (
1122
+ const pendingMinMs =
1123
+ route.options.pendingMinMs ?? this.options.defaultPendingMinMs
1124
+ const shouldPending =
1114
1125
  !preload &&
1115
1126
  pendingMs &&
1116
1127
  (route.options.pendingComponent ??
1117
1128
  this.options.defaultPendingComponent)
1118
- ) {
1119
- pendingPromise = new Promise((r) => setTimeout(r, pendingMs))
1120
- }
1121
-
1122
- if (match.isFetching) {
1123
- loadPromise = getRouteMatch(this.state, match.id)?.loadPromise
1124
- } else {
1125
- const loaderContext: LoaderFnContext = {
1126
- params: match.params,
1127
- search: match.search,
1128
- preload: !!preload,
1129
- parentMatchPromise,
1130
- abortController: match.abortController,
1131
- context: match.context,
1132
- location: this.state.location,
1133
- navigate: (opts) =>
1134
- this.navigate({ ...opts, from: match.pathname } as any),
1135
- cause: match.cause,
1136
- }
1137
-
1138
- // Default to reloading the route all the time
1139
- let shouldReload = true
1140
1129
 
1141
- let shouldReloadDeps =
1142
- typeof route.options.shouldReload === 'function'
1143
- ? route.options.shouldReload?.(loaderContext)
1144
- : !!(route.options.shouldReload ?? true)
1130
+ const fetch = async () => {
1131
+ if (match.isFetching) {
1132
+ loadPromise = getRouteMatch(this.state, match.id)?.loadPromise
1133
+ } else {
1134
+ const loaderContext: LoaderFnContext = {
1135
+ params: match.params,
1136
+ search: match.search,
1137
+ preload: !!preload,
1138
+ parentMatchPromise,
1139
+ abortController: match.abortController,
1140
+ context: match.context,
1141
+ location: this.state.location,
1142
+ navigate: (opts) =>
1143
+ this.navigate({ ...opts, from: match.pathname } as any),
1144
+ cause: preload ? 'preload' : match.cause,
1145
+ }
1145
1146
 
1146
- if (match.cause === 'enter' || invalidate) {
1147
- match.shouldReloadDeps = shouldReloadDeps
1148
- } else if (match.cause === 'stay') {
1149
- if (typeof shouldReloadDeps === 'object') {
1150
- // compare the deps to see if they've changed
1151
- shouldReload = !deepEqual(
1152
- shouldReloadDeps,
1153
- match.shouldReloadDeps,
1154
- )
1147
+ // Default to reloading the route all the time
1148
+ let shouldLoad = true
1149
+
1150
+ const shouldReloadFn = route.options.shouldReload
1151
+
1152
+ let shouldReloadDeps =
1153
+ typeof shouldReloadFn === 'function'
1154
+ ? shouldReloadFn(loaderContext)
1155
+ : !!(shouldReloadFn ?? true)
1156
+
1157
+ const compareDeps = () => {
1158
+ if (typeof shouldReloadDeps === 'object') {
1159
+ // compare the deps to see if they've changed
1160
+ shouldLoad = !deepEqual(
1161
+ shouldReloadDeps,
1162
+ match.shouldReloadDeps,
1163
+ )
1164
+ } else {
1165
+ shouldLoad = !!shouldReloadDeps
1166
+ }
1167
+ }
1155
1168
 
1156
- match.shouldReloadDeps = shouldReloadDeps
1169
+ // If it's the first preload, or the route is entering, or we're
1170
+ // invalidating, we definitely need to load the route
1171
+ if (invalidate) {
1172
+ // Change nothing, we need to load the route
1173
+ } else if (preload) {
1174
+ if (!match.fetchCount) {
1175
+ // Change nothing, we need to preload the route
1176
+ } else {
1177
+ compareDeps()
1178
+ }
1179
+ } else if (match.cause === 'enter') {
1180
+ if (!match.fetchCount) {
1181
+ // Change nothing, we 100% need to load the route
1182
+ } else {
1183
+ compareDeps()
1184
+ }
1157
1185
  } else {
1158
- shouldReload = !!shouldReloadDeps
1186
+ compareDeps()
1159
1187
  }
1160
- }
1161
-
1162
- // If the user doesn't want the route to reload, just
1163
- // resolve with the existing loader data
1164
1188
 
1165
- if (!shouldReload) {
1166
- loadPromise = Promise.resolve(match.loaderData)
1167
- } else {
1168
- // Otherwise, load the route
1169
- matches[index] = match = {
1170
- ...match,
1171
- isFetching: true,
1189
+ if (typeof shouldReloadDeps === 'object') {
1190
+ matches[index] = match = {
1191
+ ...match,
1192
+ shouldReloadDeps,
1193
+ }
1172
1194
  }
1173
1195
 
1174
- const componentsPromise = Promise.all(
1175
- componentTypes.map(async (type) => {
1176
- const component = route.options[type]
1177
-
1178
- if ((component as any)?.preload) {
1179
- await (component as any).preload()
1180
- }
1181
- }),
1182
- )
1183
-
1184
- const loaderPromise = route.options.loader?.(loaderContext)
1196
+ // If the user doesn't want the route to reload, just
1197
+ // resolve with the existing loader data
1185
1198
 
1186
- loadPromise = Promise.all([
1187
- componentsPromise,
1188
- loaderPromise,
1189
- ]).then((d) => d[1])
1190
- }
1191
- }
1199
+ if (!shouldLoad) {
1200
+ loadPromise = Promise.resolve(match.loaderData)
1201
+ } else {
1202
+ if (match.fetchCount && match.status === 'success') {
1203
+ resolve()
1204
+ }
1192
1205
 
1193
- matches[index] = match = {
1194
- ...match,
1195
- loadPromise,
1196
- }
1206
+ // Otherwise, load the route
1207
+ matches[index] = match = {
1208
+ ...match,
1209
+ isFetching: true,
1210
+ fetchCount: match.fetchCount + 1,
1211
+ }
1197
1212
 
1198
- if (!preload) {
1199
- updatePendingMatch(match)
1200
- }
1213
+ const componentsPromise = Promise.all(
1214
+ componentTypes.map(async (type) => {
1215
+ const component = route.options[type]
1201
1216
 
1202
- let didShowPending = false
1203
- const pendingMinMs =
1204
- route.options.pendingMinMs ?? this.options.defaultPendingMinMs
1217
+ if ((component as any)?.preload) {
1218
+ await (component as any).preload()
1219
+ }
1220
+ }),
1221
+ )
1205
1222
 
1206
- await new Promise<void>(async (resolve) => {
1207
- // If the route has a pending component and a pendingMs option,
1208
- // forcefully show the pending component
1209
- if (pendingPromise) {
1210
- pendingPromise.then(() => {
1211
- if ((latestPromise = checkLatest())) return
1223
+ const loaderPromise = route.options.loader?.(loaderContext)
1212
1224
 
1213
- didShowPending = true
1214
- matches[index] = match = {
1215
- ...match,
1216
- showPending: true,
1217
- }
1225
+ loadPromise = Promise.all([
1226
+ componentsPromise,
1227
+ loaderPromise,
1228
+ ]).then((d) => d[1])
1229
+ }
1230
+ }
1218
1231
 
1219
- updatePendingMatch(match)
1220
- resolve()
1221
- })
1232
+ matches[index] = match = {
1233
+ ...match,
1234
+ loadPromise,
1222
1235
  }
1223
1236
 
1237
+ updateMatch(match)
1238
+
1224
1239
  try {
1225
1240
  const loaderData = await loadPromise
1226
1241
  if ((latestPromise = checkLatest())) return await latestPromise
@@ -1267,22 +1282,44 @@ export class Router<
1267
1282
  // we already moved the pendingMatches to the matches
1268
1283
  // state, so we need to update that specific match
1269
1284
  if (didShowPending && pendingMinMs && match.showPending) {
1270
- this.__store.setState((s) => ({
1271
- ...s,
1272
- matches: s.matches?.map((d) =>
1273
- d.id === match.id ? match : d,
1274
- ),
1275
- }))
1285
+ updateMatch(match)
1276
1286
  }
1277
1287
  }
1278
1288
 
1279
- if (!preload) {
1280
- updatePendingMatch(match)
1289
+ updateMatch(match)
1290
+ }
1291
+
1292
+ if (match.fetchCount && match.status === 'success') {
1293
+ // Background Fetching
1294
+ fetch()
1295
+ } else {
1296
+ // Critical Fetching
1297
+
1298
+ // If we need to potentially show the pending component,
1299
+ // start a timer to show it after the pendingMs
1300
+ if (shouldPending) {
1301
+ new Promise((r) => setTimeout(r, pendingMs)).then(async () => {
1302
+ if ((latestPromise = checkLatest())) return latestPromise
1303
+
1304
+ didShowPending = true
1305
+ matches[index] = match = {
1306
+ ...match,
1307
+ showPending: true,
1308
+ }
1309
+
1310
+ updateMatch(match)
1311
+ resolve()
1312
+ })
1281
1313
  }
1282
1314
 
1283
- resolve()
1284
- })
1285
- })(),
1315
+ await fetch()
1316
+ }
1317
+
1318
+ resolve()
1319
+ // No Fetching
1320
+
1321
+ resolve()
1322
+ }),
1286
1323
  )
1287
1324
  })
1288
1325
 
@@ -1312,24 +1349,36 @@ export class Router<
1312
1349
  pathChanged: pathDidChange,
1313
1350
  })
1314
1351
 
1315
- // Match the routes
1316
- let pendingMatches: RouteMatch<any, any>[] = this.matchRoutes(
1317
- next.pathname,
1318
- next.search,
1319
- {
1320
- debug: true,
1321
- },
1322
- )
1323
-
1352
+ let pendingMatches!: RouteMatch<any, any>[]
1324
1353
  const previousMatches = this.state.matches
1325
1354
 
1326
- // Ingest the new matches
1327
- this.__store.setState((s) => ({
1328
- ...s,
1329
- isLoading: true,
1330
- location: next,
1331
- pendingMatches,
1332
- }))
1355
+ this.__store.batch(() => {
1356
+ this.__store.setState((s) => ({
1357
+ ...s,
1358
+ preloadMatches: s.preloadMatches.filter((d) => {
1359
+ return (
1360
+ Date.now() - d.updatedAt <
1361
+ (this.options.defaultPreloadMaxAge ?? 3000)
1362
+ )
1363
+ }),
1364
+ }))
1365
+
1366
+ // Match the routes
1367
+ pendingMatches = this.matchRoutes(next.pathname, next.search, {
1368
+ debug: true,
1369
+ })
1370
+
1371
+ // Ingest the new matches
1372
+ this.__store.setState((s) => ({
1373
+ ...s,
1374
+ isLoading: true,
1375
+ location: next,
1376
+ pendingMatches,
1377
+ preloadMatches: s.preloadMatches.filter((d) => {
1378
+ return !pendingMatches.find((e) => e.id === d.id)
1379
+ }),
1380
+ }))
1381
+ })
1333
1382
 
1334
1383
  try {
1335
1384
  try {
@@ -1411,6 +1460,25 @@ export class Router<
1411
1460
  throwOnError: true,
1412
1461
  })
1413
1462
 
1463
+ const loadedMatchIds = Object.fromEntries(
1464
+ [
1465
+ ...this.state.matches,
1466
+ ...(this.state.pendingMatches ?? []),
1467
+ ...this.state.preloadMatches,
1468
+ ]?.map((d) => [d.id, true]),
1469
+ )
1470
+
1471
+ this.__store.batch(() => {
1472
+ matches.forEach((match) => {
1473
+ if (!loadedMatchIds[match.id]) {
1474
+ this.__store.setState((s) => ({
1475
+ ...s,
1476
+ preloadMatches: [...(s.preloadMatches as any), match],
1477
+ }))
1478
+ }
1479
+ })
1480
+ })
1481
+
1414
1482
  matches = await this.loadMatches({
1415
1483
  matches,
1416
1484
  preload: true,
@@ -1500,7 +1568,7 @@ export class Router<
1500
1568
  return {
1501
1569
  state: {
1502
1570
  dehydratedMatches: this.state.matches.map((d) =>
1503
- pick(d, ['fetchedAt', 'id', 'status', 'updatedAt', 'loaderData']),
1571
+ pick(d, ['id', 'status', 'updatedAt', 'loaderData']),
1504
1572
  ),
1505
1573
  },
1506
1574
  }
@@ -1588,6 +1656,7 @@ export function getInitialRouterState(
1588
1656
  location,
1589
1657
  matches: [],
1590
1658
  pendingMatches: [],
1659
+ preloadMatches: [],
1591
1660
  lastUpdated: Date.now(),
1592
1661
  }
1593
1662
  }