@tanstack/router-core 1.131.14 → 1.131.16

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/router.ts CHANGED
@@ -763,6 +763,18 @@ export type CreateRouterFn = <
763
763
  TDehydrated
764
764
  >
765
765
 
766
+ type InnerLoadContext = {
767
+ location: ParsedLocation
768
+ firstBadMatchIndex?: number
769
+ rendered?: boolean
770
+ updateMatch: UpdateMatchFn
771
+ matches: Array<AnyRouteMatch>
772
+ preload?: boolean
773
+ onReady?: () => Promise<void>
774
+ sync?: boolean
775
+ matchPromises: Array<Promise<AnyRouteMatch>>
776
+ }
777
+
766
778
  export class RouterCore<
767
779
  in out TRouteTree extends AnyRoute,
768
780
  in out TTrailingSlashOption extends TrailingSlashOption,
@@ -2083,719 +2095,838 @@ export class RouterCore<
2083
2095
  )
2084
2096
  }
2085
2097
 
2086
- loadMatches = async ({
2087
- location,
2088
- matches,
2089
- preload: allPreload,
2090
- onReady,
2091
- updateMatch = this.updateMatch,
2092
- sync,
2093
- }: {
2094
- location: ParsedLocation
2095
- matches: Array<AnyRouteMatch>
2096
- preload?: boolean
2097
- onReady?: () => Promise<void>
2098
- updateMatch?: (
2099
- id: string,
2100
- updater: (match: AnyRouteMatch) => AnyRouteMatch,
2101
- ) => void
2102
- getMatch?: (matchId: string) => AnyRouteMatch | undefined
2103
- sync?: boolean
2104
- }): Promise<Array<MakeRouteMatch>> => {
2105
- let firstBadMatchIndex: number | undefined
2106
- let rendered = false
2098
+ private triggerOnReady = (
2099
+ innerLoadContext: InnerLoadContext,
2100
+ ): void | Promise<void> => {
2101
+ if (!innerLoadContext.rendered) {
2102
+ innerLoadContext.rendered = true
2103
+ return innerLoadContext.onReady?.()
2104
+ }
2105
+ }
2106
+
2107
+ private resolvePreload = (
2108
+ innerLoadContext: InnerLoadContext,
2109
+ matchId: string,
2110
+ ): boolean => {
2111
+ return !!(
2112
+ innerLoadContext.preload &&
2113
+ !this.state.matches.some((d) => d.id === matchId)
2114
+ )
2115
+ }
2116
+
2117
+ private handleRedirectAndNotFound = (
2118
+ innerLoadContext: InnerLoadContext,
2119
+ match: AnyRouteMatch | undefined,
2120
+ err: unknown,
2121
+ ): void => {
2122
+ if (!isRedirect(err) && !isNotFound(err)) return
2123
+
2124
+ if (isRedirect(err) && err.redirectHandled && !err.options.reloadDocument) {
2125
+ throw err
2126
+ }
2127
+
2128
+ // in case of a redirecting match during preload, the match does not exist
2129
+ if (match) {
2130
+ match._nonReactive.beforeLoadPromise?.resolve()
2131
+ match._nonReactive.loaderPromise?.resolve()
2132
+ match._nonReactive.beforeLoadPromise = undefined
2133
+ match._nonReactive.loaderPromise = undefined
2134
+
2135
+ const status = isRedirect(err) ? 'redirected' : 'notFound'
2136
+
2137
+ innerLoadContext.updateMatch(match.id, (prev) => ({
2138
+ ...prev,
2139
+ status,
2140
+ isFetching: false,
2141
+ error: err,
2142
+ }))
2107
2143
 
2108
- const triggerOnReady = async () => {
2109
- if (!rendered) {
2110
- rendered = true
2111
- await onReady?.()
2144
+ if (isNotFound(err) && !err.routeId) {
2145
+ err.routeId = match.routeId
2112
2146
  }
2147
+
2148
+ match._nonReactive.loadPromise?.resolve()
2113
2149
  }
2114
2150
 
2115
- const resolvePreload = (matchId: string) => {
2116
- return !!(allPreload && !this.state.matches.some((d) => d.id === matchId))
2151
+ if (isRedirect(err)) {
2152
+ innerLoadContext.rendered = true
2153
+ err.options._fromLocation = innerLoadContext.location
2154
+ err.redirectHandled = true
2155
+ err = this.resolveRedirect(err)
2156
+ throw err
2157
+ } else {
2158
+ this._handleNotFound(innerLoadContext, err)
2159
+ throw err
2117
2160
  }
2161
+ }
2118
2162
 
2119
- // make sure the pending component is immediately rendered when hydrating a match that is not SSRed
2120
- // the pending component was already rendered on the server and we want to keep it shown on the client until minPendingMs is reached
2121
- if (!this.isServer && this.state.matches.some((d) => d._forcePending)) {
2122
- triggerOnReady()
2163
+ private shouldSkipLoader = (matchId: string): boolean => {
2164
+ const match = this.getMatch(matchId)!
2165
+ // upon hydration, we skip the loader if the match has been dehydrated on the server
2166
+ if (!this.isServer && match._nonReactive.dehydrated) {
2167
+ return true
2123
2168
  }
2124
2169
 
2125
- const handleRedirectAndNotFound = (
2126
- match: AnyRouteMatch | undefined,
2127
- err: unknown,
2128
- ) => {
2129
- if (!isRedirect(err) && !isNotFound(err)) return
2170
+ if (this.isServer) {
2171
+ if (match.ssr === false) {
2172
+ return true
2173
+ }
2174
+ }
2175
+ return false
2176
+ }
2130
2177
 
2131
- if (
2132
- isRedirect(err) &&
2133
- err.redirectHandled &&
2134
- !err.options.reloadDocument
2178
+ private handleSerialError = (
2179
+ innerLoadContext: InnerLoadContext,
2180
+ index: number,
2181
+ err: any,
2182
+ routerCode: string,
2183
+ ): void => {
2184
+ const { id: matchId, routeId } = innerLoadContext.matches[index]!
2185
+ const route = this.looseRoutesById[routeId]!
2186
+
2187
+ // Much like suspense, we use a promise here to know if
2188
+ // we've been outdated by a new loadMatches call and
2189
+ // should abort the current async operation
2190
+ if (err instanceof Promise) {
2191
+ throw err
2192
+ }
2193
+
2194
+ err.routerCode = routerCode
2195
+ innerLoadContext.firstBadMatchIndex ??= index
2196
+ this.handleRedirectAndNotFound(
2197
+ innerLoadContext,
2198
+ this.getMatch(matchId),
2199
+ err,
2200
+ )
2201
+
2202
+ try {
2203
+ route.options.onError?.(err)
2204
+ } catch (errorHandlerErr) {
2205
+ err = errorHandlerErr
2206
+ this.handleRedirectAndNotFound(
2207
+ innerLoadContext,
2208
+ this.getMatch(matchId),
2209
+ err,
2210
+ )
2211
+ }
2212
+
2213
+ innerLoadContext.updateMatch(matchId, (prev) => {
2214
+ prev._nonReactive.beforeLoadPromise?.resolve()
2215
+ prev._nonReactive.beforeLoadPromise = undefined
2216
+ prev._nonReactive.loadPromise?.resolve()
2217
+
2218
+ return {
2219
+ ...prev,
2220
+ error: err,
2221
+ status: 'error',
2222
+ isFetching: false,
2223
+ updatedAt: Date.now(),
2224
+ abortController: new AbortController(),
2225
+ }
2226
+ })
2227
+ }
2228
+
2229
+ private isBeforeLoadSsr = (
2230
+ innerLoadContext: InnerLoadContext,
2231
+ matchId: string,
2232
+ index: number,
2233
+ route: AnyRoute,
2234
+ ): void | Promise<void> => {
2235
+ const existingMatch = this.getMatch(matchId)!
2236
+ const parentMatchId = innerLoadContext.matches[index - 1]?.id
2237
+ const parentMatch = parentMatchId
2238
+ ? this.getMatch(parentMatchId)!
2239
+ : undefined
2240
+
2241
+ // in SPA mode, only SSR the root route
2242
+ if (this.isShell()) {
2243
+ existingMatch.ssr = matchId === rootRouteId
2244
+ return
2245
+ }
2246
+
2247
+ if (parentMatch?.ssr === false) {
2248
+ existingMatch.ssr = false
2249
+ return
2250
+ }
2251
+
2252
+ const parentOverride = (tempSsr: boolean | 'data-only') => {
2253
+ if (tempSsr === true && parentMatch?.ssr === 'data-only') {
2254
+ return 'data-only'
2255
+ }
2256
+ return tempSsr
2257
+ }
2258
+
2259
+ const defaultSsr = this.options.defaultSsr ?? true
2260
+
2261
+ if (route.options.ssr === undefined) {
2262
+ existingMatch.ssr = parentOverride(defaultSsr)
2263
+ return
2264
+ }
2265
+
2266
+ if (typeof route.options.ssr !== 'function') {
2267
+ existingMatch.ssr = parentOverride(route.options.ssr)
2268
+ return
2269
+ }
2270
+ const { search, params } = this.getMatch(matchId)!
2271
+
2272
+ const ssrFnContext: SsrContextOptions<any, any, any> = {
2273
+ search: makeMaybe(search, existingMatch.searchError),
2274
+ params: makeMaybe(params, existingMatch.paramsError),
2275
+ location: innerLoadContext.location,
2276
+ matches: innerLoadContext.matches.map((match) => ({
2277
+ index: match.index,
2278
+ pathname: match.pathname,
2279
+ fullPath: match.fullPath,
2280
+ staticData: match.staticData,
2281
+ id: match.id,
2282
+ routeId: match.routeId,
2283
+ search: makeMaybe(match.search, match.searchError),
2284
+ params: makeMaybe(match.params, match.paramsError),
2285
+ ssr: match.ssr,
2286
+ })),
2287
+ }
2288
+
2289
+ const tempSsr = route.options.ssr(ssrFnContext)
2290
+ if (isPromise(tempSsr)) {
2291
+ return tempSsr.then((ssr) => {
2292
+ existingMatch.ssr = parentOverride(ssr ?? defaultSsr)
2293
+ })
2294
+ }
2295
+
2296
+ existingMatch.ssr = parentOverride(tempSsr ?? defaultSsr)
2297
+ return
2298
+ }
2299
+
2300
+ private setupPendingTimeout = (
2301
+ innerLoadContext: InnerLoadContext,
2302
+ matchId: string,
2303
+ route: AnyRoute,
2304
+ ): void => {
2305
+ const pendingMs = route.options.pendingMs ?? this.options.defaultPendingMs
2306
+ const shouldPending = !!(
2307
+ innerLoadContext.onReady &&
2308
+ !this.isServer &&
2309
+ !this.resolvePreload(innerLoadContext, matchId) &&
2310
+ (route.options.loader ||
2311
+ route.options.beforeLoad ||
2312
+ routeNeedsPreload(route)) &&
2313
+ typeof pendingMs === 'number' &&
2314
+ pendingMs !== Infinity &&
2315
+ (route.options.pendingComponent ??
2316
+ (this.options as any)?.defaultPendingComponent)
2317
+ )
2318
+ const match = this.getMatch(matchId)!
2319
+ if (shouldPending && match._nonReactive.pendingTimeout === undefined) {
2320
+ const pendingTimeout = setTimeout(() => {
2321
+ try {
2322
+ // Update the match and prematurely resolve the loadMatches promise so that
2323
+ // the pending component can start rendering
2324
+ this.triggerOnReady(innerLoadContext)
2325
+ } catch {}
2326
+ }, pendingMs)
2327
+ match._nonReactive.pendingTimeout = pendingTimeout
2328
+ }
2329
+ }
2330
+
2331
+ private shouldExecuteBeforeLoad = (
2332
+ innerLoadContext: InnerLoadContext,
2333
+ matchId: string,
2334
+ route: AnyRoute,
2335
+ ): boolean | Promise<boolean> => {
2336
+ const existingMatch = this.getMatch(matchId)!
2337
+
2338
+ // If we are in the middle of a load, either of these will be present
2339
+ // (not to be confused with `loadPromise`, which is always defined)
2340
+ if (
2341
+ !existingMatch._nonReactive.beforeLoadPromise &&
2342
+ !existingMatch._nonReactive.loaderPromise
2343
+ )
2344
+ return true
2345
+
2346
+ this.setupPendingTimeout(innerLoadContext, matchId, route)
2347
+
2348
+ const then = () => {
2349
+ let shouldExecuteBeforeLoad = true
2350
+ const match = this.getMatch(matchId)!
2351
+ if (match.status === 'error') {
2352
+ shouldExecuteBeforeLoad = true
2353
+ } else if (
2354
+ match.preload &&
2355
+ (match.status === 'redirected' || match.status === 'notFound')
2135
2356
  ) {
2136
- throw err
2357
+ this.handleRedirectAndNotFound(innerLoadContext, match, match.error)
2137
2358
  }
2359
+ return shouldExecuteBeforeLoad
2360
+ }
2138
2361
 
2139
- // in case of a redirecting match during preload, the match does not exist
2140
- if (match) {
2141
- match._nonReactive.beforeLoadPromise?.resolve()
2142
- match._nonReactive.loaderPromise?.resolve()
2143
- match._nonReactive.beforeLoadPromise = undefined
2144
- match._nonReactive.loaderPromise = undefined
2362
+ // Wait for the beforeLoad to resolve before we continue
2363
+ return existingMatch._nonReactive.beforeLoadPromise
2364
+ ? existingMatch._nonReactive.beforeLoadPromise.then(then)
2365
+ : then()
2366
+ }
2145
2367
 
2146
- const status = isRedirect(err) ? 'redirected' : 'notFound'
2368
+ private executeBeforeLoad = (
2369
+ innerLoadContext: InnerLoadContext,
2370
+ matchId: string,
2371
+ index: number,
2372
+ route: AnyRoute,
2373
+ ): void | Promise<void> => {
2374
+ const resolve = () => {
2375
+ innerLoadContext.updateMatch(matchId, (prev) => {
2376
+ prev._nonReactive.beforeLoadPromise?.resolve()
2377
+ prev._nonReactive.beforeLoadPromise = undefined
2147
2378
 
2148
- updateMatch(match.id, (prev) => ({
2379
+ return {
2149
2380
  ...prev,
2150
- status,
2151
2381
  isFetching: false,
2152
- error: err,
2153
- }))
2382
+ }
2383
+ })
2384
+ }
2385
+
2386
+ try {
2387
+ const match = this.getMatch(matchId)!
2388
+ match._nonReactive.beforeLoadPromise = createControlledPromise<void>()
2389
+ // explicitly capture the previous loadPromise
2390
+ const prevLoadPromise = match._nonReactive.loadPromise
2391
+ match._nonReactive.loadPromise = createControlledPromise<void>(() => {
2392
+ prevLoadPromise?.resolve()
2393
+ })
2394
+
2395
+ const { paramsError, searchError } = this.getMatch(matchId)!
2396
+
2397
+ if (paramsError) {
2398
+ this.handleSerialError(
2399
+ innerLoadContext,
2400
+ index,
2401
+ paramsError,
2402
+ 'PARSE_PARAMS',
2403
+ )
2404
+ }
2405
+
2406
+ if (searchError) {
2407
+ this.handleSerialError(
2408
+ innerLoadContext,
2409
+ index,
2410
+ searchError,
2411
+ 'VALIDATE_SEARCH',
2412
+ )
2413
+ }
2414
+
2415
+ this.setupPendingTimeout(innerLoadContext, matchId, route)
2416
+
2417
+ const abortController = new AbortController()
2418
+
2419
+ const parentMatchId = innerLoadContext.matches[index - 1]?.id
2420
+ const parentMatch = parentMatchId
2421
+ ? this.getMatch(parentMatchId)!
2422
+ : undefined
2423
+ const parentMatchContext =
2424
+ parentMatch?.context ?? this.options.context ?? undefined
2425
+
2426
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2427
+ ...prev,
2428
+ isFetching: 'beforeLoad',
2429
+ fetchCount: prev.fetchCount + 1,
2430
+ abortController,
2431
+ context: {
2432
+ ...parentMatchContext,
2433
+ ...prev.__routeContext,
2434
+ },
2435
+ }))
2436
+
2437
+ const { search, params, context, cause } = this.getMatch(matchId)!
2154
2438
 
2155
- if (isNotFound(err) && !err.routeId) {
2156
- err.routeId = match.routeId
2439
+ const preload = this.resolvePreload(innerLoadContext, matchId)
2440
+
2441
+ const beforeLoadFnContext: BeforeLoadContextOptions<
2442
+ any,
2443
+ any,
2444
+ any,
2445
+ any,
2446
+ any
2447
+ > = {
2448
+ search,
2449
+ abortController,
2450
+ params,
2451
+ preload,
2452
+ context,
2453
+ location: innerLoadContext.location,
2454
+ navigate: (opts: any) =>
2455
+ this.navigate({ ...opts, _fromLocation: innerLoadContext.location }),
2456
+ buildLocation: this.buildLocation,
2457
+ cause: preload ? 'preload' : cause,
2458
+ matches: innerLoadContext.matches,
2459
+ }
2460
+
2461
+ const updateContext = (beforeLoadContext: any) => {
2462
+ if (isRedirect(beforeLoadContext) || isNotFound(beforeLoadContext)) {
2463
+ this.handleSerialError(
2464
+ innerLoadContext,
2465
+ index,
2466
+ beforeLoadContext,
2467
+ 'BEFORE_LOAD',
2468
+ )
2157
2469
  }
2158
2470
 
2159
- match._nonReactive.loadPromise?.resolve()
2471
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2472
+ ...prev,
2473
+ __beforeLoadContext: beforeLoadContext,
2474
+ context: {
2475
+ ...parentMatchContext,
2476
+ ...prev.__routeContext,
2477
+ ...beforeLoadContext,
2478
+ },
2479
+ abortController,
2480
+ }))
2160
2481
  }
2161
2482
 
2162
- if (isRedirect(err)) {
2163
- rendered = true
2164
- err.options._fromLocation = location
2165
- err.redirectHandled = true
2166
- err = this.resolveRedirect(err)
2167
- throw err
2483
+ const beforeLoadContext = route.options.beforeLoad?.(beforeLoadFnContext)
2484
+ if (isPromise(beforeLoadContext)) {
2485
+ return beforeLoadContext
2486
+ .then(updateContext)
2487
+ .catch((err) => {
2488
+ this.handleSerialError(innerLoadContext, index, err, 'BEFORE_LOAD')
2489
+ })
2490
+ .then(resolve)
2168
2491
  } else {
2169
- this._handleNotFound(matches, err, updateMatch)
2170
- throw err
2492
+ updateContext(beforeLoadContext)
2171
2493
  }
2494
+ } catch (err) {
2495
+ this.handleSerialError(innerLoadContext, index, err, 'BEFORE_LOAD')
2172
2496
  }
2173
2497
 
2174
- const shouldSkipLoader = (matchId: string) => {
2175
- const match = this.getMatch(matchId)!
2176
- // upon hydration, we skip the loader if the match has been dehydrated on the server
2177
- if (!this.isServer && match._nonReactive.dehydrated) {
2178
- return true
2179
- }
2498
+ resolve()
2499
+ return
2500
+ }
2501
+
2502
+ private handleBeforeLoad = (
2503
+ innerLoadContext: InnerLoadContext,
2504
+ index: number,
2505
+ ): void | Promise<void> => {
2506
+ const { id: matchId, routeId } = innerLoadContext.matches[index]!
2507
+ const route = this.looseRoutesById[routeId]!
2180
2508
 
2509
+ const serverSsr = () => {
2510
+ // on the server, determine whether SSR the current match or not
2181
2511
  if (this.isServer) {
2182
- if (match.ssr === false) {
2183
- return true
2184
- }
2512
+ const maybePromise = this.isBeforeLoadSsr(
2513
+ innerLoadContext,
2514
+ matchId,
2515
+ index,
2516
+ route,
2517
+ )
2518
+ if (isPromise(maybePromise)) return maybePromise.then(queueExecution)
2185
2519
  }
2186
- return false
2520
+ return queueExecution()
2521
+ }
2522
+
2523
+ const queueExecution = () => {
2524
+ if (this.shouldSkipLoader(matchId)) return
2525
+ const shouldExecuteBeforeLoadResult = this.shouldExecuteBeforeLoad(
2526
+ innerLoadContext,
2527
+ matchId,
2528
+ route,
2529
+ )
2530
+ return isPromise(shouldExecuteBeforeLoadResult)
2531
+ ? shouldExecuteBeforeLoadResult.then(execute)
2532
+ : execute(shouldExecuteBeforeLoadResult)
2533
+ }
2534
+
2535
+ const execute = (shouldExecuteBeforeLoad: boolean) => {
2536
+ if (shouldExecuteBeforeLoad) {
2537
+ // If we are not in the middle of a load OR the previous load failed, start it
2538
+ return this.executeBeforeLoad(innerLoadContext, matchId, index, route)
2539
+ }
2540
+ return
2187
2541
  }
2188
2542
 
2543
+ return serverSsr()
2544
+ }
2545
+
2546
+ private executeHead = (
2547
+ innerLoadContext: InnerLoadContext,
2548
+ matchId: string,
2549
+ route: AnyRoute,
2550
+ ): void | Promise<
2551
+ Pick<
2552
+ AnyRouteMatch,
2553
+ 'meta' | 'links' | 'headScripts' | 'headers' | 'scripts' | 'styles'
2554
+ >
2555
+ > => {
2556
+ const match = this.getMatch(matchId)
2557
+ // in case of a redirecting match during preload, the match does not exist
2558
+ if (!match) {
2559
+ return
2560
+ }
2561
+ if (
2562
+ !route.options.head &&
2563
+ !route.options.scripts &&
2564
+ !route.options.headers
2565
+ ) {
2566
+ return
2567
+ }
2568
+ const assetContext = {
2569
+ matches: innerLoadContext.matches,
2570
+ match,
2571
+ params: match.params,
2572
+ loaderData: match.loaderData,
2573
+ }
2574
+
2575
+ return Promise.all([
2576
+ route.options.head?.(assetContext),
2577
+ route.options.scripts?.(assetContext),
2578
+ route.options.headers?.(assetContext),
2579
+ ]).then(([headFnContent, scripts, headers]) => {
2580
+ const meta = headFnContent?.meta
2581
+ const links = headFnContent?.links
2582
+ const headScripts = headFnContent?.scripts
2583
+ const styles = headFnContent?.styles
2584
+
2585
+ return {
2586
+ meta,
2587
+ links,
2588
+ headScripts,
2589
+ headers,
2590
+ scripts,
2591
+ styles,
2592
+ }
2593
+ })
2594
+ }
2595
+
2596
+ private potentialPendingMinPromise = (
2597
+ matchId: string,
2598
+ ): void | ControlledPromise<void> => {
2599
+ const latestMatch = this.getMatch(matchId)!
2600
+ return latestMatch._nonReactive.minPendingPromise
2601
+ }
2602
+
2603
+ private getLoaderContext = (
2604
+ innerLoadContext: InnerLoadContext,
2605
+ matchId: string,
2606
+ index: number,
2607
+ route: AnyRoute,
2608
+ ): LoaderFnContext => {
2609
+ const parentMatchPromise = innerLoadContext.matchPromises[index - 1] as any
2610
+ const { params, loaderDeps, abortController, context, cause } =
2611
+ this.getMatch(matchId)!
2612
+
2613
+ const preload = this.resolvePreload(innerLoadContext, matchId)
2614
+
2615
+ return {
2616
+ params,
2617
+ deps: loaderDeps,
2618
+ preload: !!preload,
2619
+ parentMatchPromise,
2620
+ abortController: abortController,
2621
+ context,
2622
+ location: innerLoadContext.location,
2623
+ navigate: (opts) =>
2624
+ this.navigate({ ...opts, _fromLocation: innerLoadContext.location }),
2625
+ cause: preload ? 'preload' : cause,
2626
+ route,
2627
+ }
2628
+ }
2629
+
2630
+ private runLoader = async (
2631
+ innerLoadContext: InnerLoadContext,
2632
+ matchId: string,
2633
+ index: number,
2634
+ route: AnyRoute,
2635
+ ): Promise<void> => {
2189
2636
  try {
2190
- await new Promise<void>((resolveAll, rejectAll) => {
2191
- ;(async () => {
2192
- try {
2193
- const handleSerialError = (
2194
- index: number,
2195
- err: any,
2196
- routerCode: string,
2197
- ) => {
2198
- const { id: matchId, routeId } = matches[index]!
2199
- const route = this.looseRoutesById[routeId]!
2200
-
2201
- // Much like suspense, we use a promise here to know if
2202
- // we've been outdated by a new loadMatches call and
2203
- // should abort the current async operation
2204
- if (err instanceof Promise) {
2205
- throw err
2206
- }
2637
+ // If the Matches component rendered
2638
+ // the pending component and needs to show it for
2639
+ // a minimum duration, we''ll wait for it to resolve
2640
+ // before committing to the match and resolving
2641
+ // the loadPromise
2207
2642
 
2208
- err.routerCode = routerCode
2209
- firstBadMatchIndex = firstBadMatchIndex ?? index
2210
- handleRedirectAndNotFound(this.getMatch(matchId), err)
2643
+ // Actually run the loader and handle the result
2644
+ try {
2645
+ if (!this.isServer || this.getMatch(matchId)!.ssr === true) {
2646
+ this.loadRouteChunk(route)
2647
+ }
2211
2648
 
2212
- try {
2213
- route.options.onError?.(err)
2214
- } catch (errorHandlerErr) {
2215
- err = errorHandlerErr
2216
- handleRedirectAndNotFound(this.getMatch(matchId), err)
2217
- }
2649
+ // Kick off the loader!
2650
+ const loaderResult = route.options.loader?.(
2651
+ this.getLoaderContext(innerLoadContext, matchId, index, route),
2652
+ )
2653
+ const loaderResultIsPromise =
2654
+ route.options.loader && isPromise(loaderResult)
2655
+
2656
+ const willLoadSomething = !!(
2657
+ loaderResultIsPromise ||
2658
+ route._lazyPromise ||
2659
+ route._componentsPromise ||
2660
+ route.options.head ||
2661
+ route.options.scripts ||
2662
+ route.options.headers ||
2663
+ this.getMatch(matchId)!._nonReactive.minPendingPromise
2664
+ )
2218
2665
 
2219
- updateMatch(matchId, (prev) => {
2220
- prev._nonReactive.beforeLoadPromise?.resolve()
2221
- prev._nonReactive.beforeLoadPromise = undefined
2222
- prev._nonReactive.loadPromise?.resolve()
2223
-
2224
- return {
2225
- ...prev,
2226
- error: err,
2227
- status: 'error',
2228
- isFetching: false,
2229
- updatedAt: Date.now(),
2230
- abortController: new AbortController(),
2231
- }
2232
- })
2233
- }
2666
+ if (willLoadSomething) {
2667
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2668
+ ...prev,
2669
+ isFetching: 'loader',
2670
+ }))
2671
+ }
2234
2672
 
2235
- for (const [index, { id: matchId, routeId }] of matches.entries()) {
2236
- const existingMatch = this.getMatch(matchId)!
2237
- const parentMatchId = matches[index - 1]?.id
2238
- const parentMatch = parentMatchId
2239
- ? this.getMatch(parentMatchId)!
2240
- : undefined
2241
-
2242
- const route = this.looseRoutesById[routeId]!
2243
-
2244
- const pendingMs =
2245
- route.options.pendingMs ?? this.options.defaultPendingMs
2246
-
2247
- // on the server, determine whether SSR the current match or not
2248
- if (this.isServer) {
2249
- let ssr: boolean | 'data-only'
2250
- // in SPA mode, only SSR the root route
2251
- if (this.isShell()) {
2252
- ssr = matchId === rootRouteId
2253
- } else {
2254
- const defaultSsr = this.options.defaultSsr ?? true
2255
- if (parentMatch?.ssr === false) {
2256
- ssr = false
2257
- } else {
2258
- let tempSsr: boolean | 'data-only'
2259
- if (route.options.ssr === undefined) {
2260
- tempSsr = defaultSsr
2261
- } else if (typeof route.options.ssr === 'function') {
2262
- const { search, params } = this.getMatch(matchId)!
2263
-
2264
- function makeMaybe(value: any, error: any) {
2265
- if (error) {
2266
- return { status: 'error' as const, error }
2267
- }
2268
- return { status: 'success' as const, value }
2269
- }
2270
-
2271
- const ssrFnContext: SsrContextOptions<any, any, any> = {
2272
- search: makeMaybe(search, existingMatch.searchError),
2273
- params: makeMaybe(params, existingMatch.paramsError),
2274
- location,
2275
- matches: matches.map((match) => ({
2276
- index: match.index,
2277
- pathname: match.pathname,
2278
- fullPath: match.fullPath,
2279
- staticData: match.staticData,
2280
- id: match.id,
2281
- routeId: match.routeId,
2282
- search: makeMaybe(match.search, match.searchError),
2283
- params: makeMaybe(match.params, match.paramsError),
2284
- ssr: match.ssr,
2285
- })),
2286
- }
2287
- tempSsr =
2288
- (await route.options.ssr(ssrFnContext)) ?? defaultSsr
2289
- } else {
2290
- tempSsr = route.options.ssr
2291
- }
2673
+ if (route.options.loader) {
2674
+ const loaderData = loaderResultIsPromise
2675
+ ? await loaderResult
2676
+ : loaderResult
2292
2677
 
2293
- if (tempSsr === true && parentMatch?.ssr === 'data-only') {
2294
- ssr = 'data-only'
2295
- } else {
2296
- ssr = tempSsr
2297
- }
2298
- }
2299
- }
2300
- existingMatch.ssr = ssr
2301
- }
2678
+ this.handleRedirectAndNotFound(
2679
+ innerLoadContext,
2680
+ this.getMatch(matchId),
2681
+ loaderData,
2682
+ )
2683
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2684
+ ...prev,
2685
+ loaderData,
2686
+ }))
2687
+ }
2302
2688
 
2303
- if (shouldSkipLoader(matchId)) {
2304
- continue
2305
- }
2689
+ // Lazy option can modify the route options,
2690
+ // so we need to wait for it to resolve before
2691
+ // we can use the options
2692
+ if (route._lazyPromise) await route._lazyPromise
2693
+ const headResult = this.executeHead(innerLoadContext, matchId, route)
2694
+ const head = headResult ? await headResult : undefined
2695
+ const pendingPromise = this.potentialPendingMinPromise(matchId)
2696
+ if (pendingPromise) await pendingPromise
2697
+
2698
+ // Last but not least, wait for the the components
2699
+ // to be preloaded before we resolve the match
2700
+ if (route._componentsPromise) await route._componentsPromise
2701
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2702
+ ...prev,
2703
+ error: undefined,
2704
+ status: 'success',
2705
+ isFetching: false,
2706
+ updatedAt: Date.now(),
2707
+ ...head,
2708
+ }))
2709
+ } catch (e) {
2710
+ let error = e
2306
2711
 
2307
- const shouldPending = !!(
2308
- onReady &&
2309
- !this.isServer &&
2310
- !resolvePreload(matchId) &&
2311
- (route.options.loader ||
2312
- route.options.beforeLoad ||
2313
- routeNeedsPreload(route)) &&
2314
- typeof pendingMs === 'number' &&
2315
- pendingMs !== Infinity &&
2316
- (route.options.pendingComponent ??
2317
- (this.options as any)?.defaultPendingComponent)
2318
- )
2712
+ await this.potentialPendingMinPromise(matchId)
2319
2713
 
2320
- let executeBeforeLoad = true
2321
- const setupPendingTimeout = () => {
2322
- const match = this.getMatch(matchId)!
2323
- if (
2324
- shouldPending &&
2325
- match._nonReactive.pendingTimeout === undefined
2326
- ) {
2327
- const pendingTimeout = setTimeout(() => {
2328
- try {
2329
- // Update the match and prematurely resolve the loadMatches promise so that
2330
- // the pending component can start rendering
2331
- triggerOnReady()
2332
- } catch {}
2333
- }, pendingMs)
2334
- match._nonReactive.pendingTimeout = pendingTimeout
2335
- }
2336
- }
2337
- if (
2338
- // If we are in the middle of a load, either of these will be present
2339
- // (not to be confused with `loadPromise`, which is always defined)
2340
- existingMatch._nonReactive.beforeLoadPromise ||
2341
- existingMatch._nonReactive.loaderPromise
2342
- ) {
2343
- setupPendingTimeout()
2344
-
2345
- // Wait for the beforeLoad to resolve before we continue
2346
- await existingMatch._nonReactive.beforeLoadPromise
2347
- const match = this.getMatch(matchId)!
2348
- if (match.status === 'error') {
2349
- executeBeforeLoad = true
2350
- } else if (
2351
- match.preload &&
2352
- (match.status === 'redirected' || match.status === 'notFound')
2353
- ) {
2354
- handleRedirectAndNotFound(match, match.error)
2355
- }
2356
- }
2357
- if (executeBeforeLoad) {
2358
- // If we are not in the middle of a load OR the previous load failed, start it
2359
- try {
2360
- const match = this.getMatch(matchId)!
2361
- match._nonReactive.beforeLoadPromise =
2362
- createControlledPromise<void>()
2363
- // explicitly capture the previous loadPromise
2364
- const prevLoadPromise = match._nonReactive.loadPromise
2365
- match._nonReactive.loadPromise =
2366
- createControlledPromise<void>(() => {
2367
- prevLoadPromise?.resolve()
2368
- })
2369
-
2370
- const { paramsError, searchError } = this.getMatch(matchId)!
2371
-
2372
- if (paramsError) {
2373
- handleSerialError(index, paramsError, 'PARSE_PARAMS')
2374
- }
2375
-
2376
- if (searchError) {
2377
- handleSerialError(index, searchError, 'VALIDATE_SEARCH')
2378
- }
2379
-
2380
- setupPendingTimeout()
2381
-
2382
- const abortController = new AbortController()
2383
-
2384
- const parentMatchContext =
2385
- parentMatch?.context ?? this.options.context ?? undefined
2386
-
2387
- updateMatch(matchId, (prev) => ({
2388
- ...prev,
2389
- isFetching: 'beforeLoad',
2390
- fetchCount: prev.fetchCount + 1,
2391
- abortController,
2392
- context: {
2393
- ...parentMatchContext,
2394
- ...prev.__routeContext,
2395
- },
2396
- }))
2397
-
2398
- const { search, params, context, cause } =
2399
- this.getMatch(matchId)!
2400
-
2401
- const preload = resolvePreload(matchId)
2402
-
2403
- const beforeLoadFnContext: BeforeLoadContextOptions<
2404
- any,
2405
- any,
2406
- any,
2407
- any,
2408
- any
2409
- > = {
2410
- search,
2411
- abortController,
2412
- params,
2413
- preload,
2414
- context,
2415
- location,
2416
- navigate: (opts: any) =>
2417
- this.navigate({ ...opts, _fromLocation: location }),
2418
- buildLocation: this.buildLocation,
2419
- cause: preload ? 'preload' : cause,
2420
- matches,
2421
- }
2422
-
2423
- const beforeLoadContext =
2424
- await route.options.beforeLoad?.(beforeLoadFnContext)
2425
-
2426
- if (
2427
- isRedirect(beforeLoadContext) ||
2428
- isNotFound(beforeLoadContext)
2429
- ) {
2430
- handleSerialError(index, beforeLoadContext, 'BEFORE_LOAD')
2431
- }
2432
-
2433
- updateMatch(matchId, (prev) => {
2434
- return {
2435
- ...prev,
2436
- __beforeLoadContext: beforeLoadContext,
2437
- context: {
2438
- ...parentMatchContext,
2439
- ...prev.__routeContext,
2440
- ...beforeLoadContext,
2441
- },
2442
- abortController,
2443
- }
2444
- })
2445
- } catch (err) {
2446
- handleSerialError(index, err, 'BEFORE_LOAD')
2447
- }
2448
-
2449
- updateMatch(matchId, (prev) => {
2450
- prev._nonReactive.beforeLoadPromise?.resolve()
2451
- prev._nonReactive.beforeLoadPromise = undefined
2452
-
2453
- return {
2454
- ...prev,
2455
- isFetching: false,
2456
- }
2457
- })
2458
- }
2714
+ this.handleRedirectAndNotFound(
2715
+ innerLoadContext,
2716
+ this.getMatch(matchId),
2717
+ e,
2718
+ )
2719
+
2720
+ try {
2721
+ route.options.onError?.(e)
2722
+ } catch (onErrorError) {
2723
+ error = onErrorError
2724
+ this.handleRedirectAndNotFound(
2725
+ innerLoadContext,
2726
+ this.getMatch(matchId),
2727
+ onErrorError,
2728
+ )
2729
+ }
2730
+ const headResult = this.executeHead(innerLoadContext, matchId, route)
2731
+ const head = headResult ? await headResult : undefined
2732
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2733
+ ...prev,
2734
+ error,
2735
+ status: 'error',
2736
+ isFetching: false,
2737
+ ...head,
2738
+ }))
2739
+ }
2740
+ } catch (err) {
2741
+ const match = this.getMatch(matchId)
2742
+ // in case of a redirecting match during preload, the match does not exist
2743
+ if (match) {
2744
+ const headResult = this.executeHead(innerLoadContext, matchId, route)
2745
+ if (headResult) {
2746
+ const head = await headResult
2747
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2748
+ ...prev,
2749
+ ...head,
2750
+ }))
2751
+ }
2752
+ match._nonReactive.loaderPromise = undefined
2753
+ }
2754
+ this.handleRedirectAndNotFound(innerLoadContext, match, err)
2755
+ }
2756
+ }
2757
+
2758
+ private loadRouteMatch = async (
2759
+ innerLoadContext: InnerLoadContext,
2760
+ index: number,
2761
+ ): Promise<AnyRouteMatch> => {
2762
+ const { id: matchId, routeId } = innerLoadContext.matches[index]!
2763
+ let loaderShouldRunAsync = false
2764
+ let loaderIsRunningAsync = false
2765
+ const route = this.looseRoutesById[routeId]!
2766
+
2767
+ const prevMatch = this.getMatch(matchId)!
2768
+ if (this.shouldSkipLoader(matchId)) {
2769
+ if (this.isServer) {
2770
+ const headResult = this.executeHead(innerLoadContext, matchId, route)
2771
+ if (headResult) {
2772
+ const head = await headResult
2773
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2774
+ ...prev,
2775
+ ...head,
2776
+ }))
2777
+ }
2778
+ return this.getMatch(matchId)!
2779
+ }
2780
+ }
2781
+ // there is a loaderPromise, so we are in the middle of a load
2782
+ else if (prevMatch._nonReactive.loaderPromise) {
2783
+ // do not block if we already have stale data we can show
2784
+ // but only if the ongoing load is not a preload since error handling is different for preloads
2785
+ // and we don't want to swallow errors
2786
+ if (
2787
+ prevMatch.status === 'success' &&
2788
+ !innerLoadContext.sync &&
2789
+ !prevMatch.preload
2790
+ ) {
2791
+ return this.getMatch(matchId)!
2792
+ }
2793
+ await prevMatch._nonReactive.loaderPromise
2794
+ const match = this.getMatch(matchId)!
2795
+ if (match.error) {
2796
+ this.handleRedirectAndNotFound(innerLoadContext, match, match.error)
2797
+ }
2798
+ } else {
2799
+ // This is where all of the stale-while-revalidate magic happens
2800
+ const age = Date.now() - this.getMatch(matchId)!.updatedAt
2801
+
2802
+ const preload = this.resolvePreload(innerLoadContext, matchId)
2803
+
2804
+ const staleAge = preload
2805
+ ? (route.options.preloadStaleTime ??
2806
+ this.options.defaultPreloadStaleTime ??
2807
+ 30_000) // 30 seconds for preloads by default
2808
+ : (route.options.staleTime ?? this.options.defaultStaleTime ?? 0)
2809
+
2810
+ const shouldReloadOption = route.options.shouldReload
2811
+
2812
+ // Default to reloading the route all the time
2813
+ // Allow shouldReload to get the last say,
2814
+ // if provided.
2815
+ const shouldReload =
2816
+ typeof shouldReloadOption === 'function'
2817
+ ? shouldReloadOption(
2818
+ this.getLoaderContext(innerLoadContext, matchId, index, route),
2819
+ )
2820
+ : shouldReloadOption
2821
+
2822
+ innerLoadContext.updateMatch(matchId, (prev) => {
2823
+ prev._nonReactive.loaderPromise = createControlledPromise<void>()
2824
+ return {
2825
+ ...prev,
2826
+ preload:
2827
+ !!preload && !this.state.matches.some((d) => d.id === matchId),
2828
+ }
2829
+ })
2830
+
2831
+ // If the route is successful and still fresh, just resolve
2832
+ const { status, invalid } = this.getMatch(matchId)!
2833
+ loaderShouldRunAsync =
2834
+ status === 'success' && (invalid || (shouldReload ?? age > staleAge))
2835
+ if (preload && route.options.preload === false) {
2836
+ // Do nothing
2837
+ } else if (loaderShouldRunAsync && !innerLoadContext.sync) {
2838
+ loaderIsRunningAsync = true
2839
+ ;(async () => {
2840
+ try {
2841
+ await this.runLoader(innerLoadContext, matchId, index, route)
2842
+ const match = this.getMatch(matchId)!
2843
+ match._nonReactive.loaderPromise?.resolve()
2844
+ match._nonReactive.loadPromise?.resolve()
2845
+ match._nonReactive.loaderPromise = undefined
2846
+ } catch (err) {
2847
+ if (isRedirect(err)) {
2848
+ await this.navigate(err.options)
2459
2849
  }
2850
+ }
2851
+ })()
2852
+ } else if (
2853
+ status !== 'success' ||
2854
+ (loaderShouldRunAsync && innerLoadContext.sync)
2855
+ ) {
2856
+ await this.runLoader(innerLoadContext, matchId, index, route)
2857
+ } else {
2858
+ // if the loader did not run, still update head.
2859
+ // reason: parent's beforeLoad may have changed the route context
2860
+ // and only now do we know the route context (and that the loader would not run)
2861
+ const headResult = this.executeHead(innerLoadContext, matchId, route)
2862
+ if (headResult) {
2863
+ const head = await headResult
2864
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2865
+ ...prev,
2866
+ ...head,
2867
+ }))
2868
+ }
2869
+ }
2870
+ }
2871
+ if (!loaderIsRunningAsync) {
2872
+ const match = this.getMatch(matchId)!
2873
+ match._nonReactive.loaderPromise?.resolve()
2874
+ match._nonReactive.loadPromise?.resolve()
2875
+ }
2460
2876
 
2461
- const validResolvedMatches = matches.slice(0, firstBadMatchIndex)
2462
- const matchPromises: Array<Promise<AnyRouteMatch>> = []
2463
-
2464
- validResolvedMatches.forEach(({ id: matchId, routeId }, index) => {
2465
- matchPromises.push(
2466
- (async () => {
2467
- let loaderShouldRunAsync = false
2468
- let loaderIsRunningAsync = false
2469
- const route = this.looseRoutesById[routeId]!
2470
-
2471
- const executeHead = () => {
2472
- const match = this.getMatch(matchId)
2473
- // in case of a redirecting match during preload, the match does not exist
2474
- if (!match) {
2475
- return
2476
- }
2477
- if (
2478
- !route.options.head &&
2479
- !route.options.scripts &&
2480
- !route.options.headers
2481
- ) {
2482
- return
2483
- }
2484
- const assetContext = {
2485
- matches,
2486
- match,
2487
- params: match.params,
2488
- loaderData: match.loaderData,
2489
- }
2877
+ innerLoadContext.updateMatch(matchId, (prev) => {
2878
+ clearTimeout(prev._nonReactive.pendingTimeout)
2879
+ prev._nonReactive.pendingTimeout = undefined
2880
+ if (!loaderIsRunningAsync) prev._nonReactive.loaderPromise = undefined
2881
+ prev._nonReactive.dehydrated = undefined
2882
+ return {
2883
+ ...prev,
2884
+ isFetching: loaderIsRunningAsync ? prev.isFetching : false,
2885
+ invalid: false,
2886
+ }
2887
+ })
2888
+ return this.getMatch(matchId)!
2889
+ }
2490
2890
 
2491
- return Promise.all([
2492
- route.options.head?.(assetContext),
2493
- route.options.scripts?.(assetContext),
2494
- route.options.headers?.(assetContext),
2495
- ]).then(([headFnContent, scripts, headers]) => {
2496
- const meta = headFnContent?.meta
2497
- const links = headFnContent?.links
2498
- const headScripts = headFnContent?.scripts
2499
- const styles = headFnContent?.styles
2500
-
2501
- return {
2502
- meta,
2503
- links,
2504
- headScripts,
2505
- headers,
2506
- scripts,
2507
- styles,
2508
- }
2509
- })
2510
- }
2511
-
2512
- const potentialPendingMinPromise = () => {
2513
- const latestMatch = this.getMatch(matchId)!
2514
- return latestMatch._nonReactive.minPendingPromise
2515
- }
2516
-
2517
- const prevMatch = this.getMatch(matchId)!
2518
- if (shouldSkipLoader(matchId)) {
2519
- if (this.isServer) {
2520
- const headResult = executeHead()
2521
- if (headResult) {
2522
- const head = await headResult
2523
- updateMatch(matchId, (prev) => ({
2524
- ...prev,
2525
- ...head,
2526
- }))
2527
- }
2528
- return this.getMatch(matchId)!
2529
- }
2530
- }
2531
- // there is a loaderPromise, so we are in the middle of a load
2532
- else if (prevMatch._nonReactive.loaderPromise) {
2533
- // do not block if we already have stale data we can show
2534
- // but only if the ongoing load is not a preload since error handling is different for preloads
2535
- // and we don't want to swallow errors
2536
- if (
2537
- prevMatch.status === 'success' &&
2538
- !sync &&
2539
- !prevMatch.preload
2540
- ) {
2541
- return this.getMatch(matchId)!
2542
- }
2543
- await prevMatch._nonReactive.loaderPromise
2544
- const match = this.getMatch(matchId)!
2545
- if (match.error) {
2546
- handleRedirectAndNotFound(match, match.error)
2547
- }
2548
- } else {
2549
- const parentMatchPromise = matchPromises[index - 1] as any
2550
-
2551
- const getLoaderContext = (): LoaderFnContext => {
2552
- const {
2553
- params,
2554
- loaderDeps,
2555
- abortController,
2556
- context,
2557
- cause,
2558
- } = this.getMatch(matchId)!
2559
-
2560
- const preload = resolvePreload(matchId)
2561
-
2562
- return {
2563
- params,
2564
- deps: loaderDeps,
2565
- preload: !!preload,
2566
- parentMatchPromise,
2567
- abortController: abortController,
2568
- context,
2569
- location,
2570
- navigate: (opts) =>
2571
- this.navigate({ ...opts, _fromLocation: location }),
2572
- cause: preload ? 'preload' : cause,
2573
- route,
2574
- }
2575
- }
2891
+ loadMatches = async (baseContext: {
2892
+ location: ParsedLocation
2893
+ matches: Array<AnyRouteMatch>
2894
+ preload?: boolean
2895
+ onReady?: () => Promise<void>
2896
+ updateMatch?: UpdateMatchFn
2897
+ sync?: boolean
2898
+ }): Promise<Array<MakeRouteMatch>> => {
2899
+ const innerLoadContext = baseContext as InnerLoadContext
2900
+ innerLoadContext.updateMatch ??= this.updateMatch
2901
+ innerLoadContext.matchPromises = []
2576
2902
 
2577
- // This is where all of the stale-while-revalidate magic happens
2578
- const age = Date.now() - this.getMatch(matchId)!.updatedAt
2579
-
2580
- const preload = resolvePreload(matchId)
2581
-
2582
- const staleAge = preload
2583
- ? (route.options.preloadStaleTime ??
2584
- this.options.defaultPreloadStaleTime ??
2585
- 30_000) // 30 seconds for preloads by default
2586
- : (route.options.staleTime ??
2587
- this.options.defaultStaleTime ??
2588
- 0)
2589
-
2590
- const shouldReloadOption = route.options.shouldReload
2591
-
2592
- // Default to reloading the route all the time
2593
- // Allow shouldReload to get the last say,
2594
- // if provided.
2595
- const shouldReload =
2596
- typeof shouldReloadOption === 'function'
2597
- ? shouldReloadOption(getLoaderContext())
2598
- : shouldReloadOption
2599
-
2600
- updateMatch(matchId, (prev) => {
2601
- prev._nonReactive.loaderPromise =
2602
- createControlledPromise<void>()
2603
- return {
2604
- ...prev,
2605
- preload:
2606
- !!preload &&
2607
- !this.state.matches.some((d) => d.id === matchId),
2608
- }
2609
- })
2610
-
2611
- const runLoader = async () => {
2612
- try {
2613
- // If the Matches component rendered
2614
- // the pending component and needs to show it for
2615
- // a minimum duration, we''ll wait for it to resolve
2616
- // before committing to the match and resolving
2617
- // the loadPromise
2618
-
2619
- // Actually run the loader and handle the result
2620
- try {
2621
- if (
2622
- !this.isServer ||
2623
- this.getMatch(matchId)!.ssr === true
2624
- ) {
2625
- this.loadRouteChunk(route)
2626
- }
2627
-
2628
- // Kick off the loader!
2629
- const loaderResult =
2630
- route.options.loader?.(getLoaderContext())
2631
- const loaderResultIsPromise =
2632
- route.options.loader && isPromise(loaderResult)
2633
-
2634
- const willLoadSomething = !!(
2635
- loaderResultIsPromise ||
2636
- route._lazyPromise ||
2637
- route._componentsPromise ||
2638
- route.options.head ||
2639
- route.options.scripts ||
2640
- route.options.headers ||
2641
- this.getMatch(matchId)!._nonReactive
2642
- .minPendingPromise
2643
- )
2644
-
2645
- if (willLoadSomething) {
2646
- updateMatch(matchId, (prev) => ({
2647
- ...prev,
2648
- isFetching: 'loader',
2649
- }))
2650
- }
2651
-
2652
- if (route.options.loader) {
2653
- const loaderData = loaderResultIsPromise
2654
- ? await loaderResult
2655
- : loaderResult
2656
-
2657
- handleRedirectAndNotFound(
2658
- this.getMatch(matchId),
2659
- loaderData,
2660
- )
2661
- updateMatch(matchId, (prev) => ({
2662
- ...prev,
2663
- loaderData,
2664
- }))
2665
- }
2666
-
2667
- // Lazy option can modify the route options,
2668
- // so we need to wait for it to resolve before
2669
- // we can use the options
2670
- if (route._lazyPromise) await route._lazyPromise
2671
- const headResult = executeHead()
2672
- const head = headResult ? await headResult : undefined
2673
- const pendingPromise = potentialPendingMinPromise()
2674
- if (pendingPromise) await pendingPromise
2675
-
2676
- // Last but not least, wait for the the components
2677
- // to be preloaded before we resolve the match
2678
- if (route._componentsPromise)
2679
- await route._componentsPromise
2680
- updateMatch(matchId, (prev) => ({
2681
- ...prev,
2682
- error: undefined,
2683
- status: 'success',
2684
- isFetching: false,
2685
- updatedAt: Date.now(),
2686
- ...head,
2687
- }))
2688
- } catch (e) {
2689
- let error = e
2690
-
2691
- await potentialPendingMinPromise()
2692
-
2693
- handleRedirectAndNotFound(this.getMatch(matchId), e)
2694
-
2695
- try {
2696
- route.options.onError?.(e)
2697
- } catch (onErrorError) {
2698
- error = onErrorError
2699
- handleRedirectAndNotFound(
2700
- this.getMatch(matchId),
2701
- onErrorError,
2702
- )
2703
- }
2704
- const headResult = executeHead()
2705
- const head = headResult ? await headResult : undefined
2706
- updateMatch(matchId, (prev) => ({
2707
- ...prev,
2708
- error,
2709
- status: 'error',
2710
- isFetching: false,
2711
- ...head,
2712
- }))
2713
- }
2714
- } catch (err) {
2715
- const match = this.getMatch(matchId)
2716
- // in case of a redirecting match during preload, the match does not exist
2717
- if (match) {
2718
- const headResult = executeHead()
2719
- if (headResult) {
2720
- const head = await headResult
2721
- updateMatch(matchId, (prev) => ({
2722
- ...prev,
2723
- ...head,
2724
- }))
2725
- }
2726
- match._nonReactive.loaderPromise = undefined
2727
- }
2728
- handleRedirectAndNotFound(match, err)
2729
- }
2730
- }
2903
+ // make sure the pending component is immediately rendered when hydrating a match that is not SSRed
2904
+ // the pending component was already rendered on the server and we want to keep it shown on the client until minPendingMs is reached
2905
+ if (!this.isServer && this.state.matches.some((d) => d._forcePending)) {
2906
+ this.triggerOnReady(innerLoadContext)
2907
+ }
2731
2908
 
2732
- // If the route is successful and still fresh, just resolve
2733
- const { status, invalid } = this.getMatch(matchId)!
2734
- loaderShouldRunAsync =
2735
- status === 'success' &&
2736
- (invalid || (shouldReload ?? age > staleAge))
2737
- if (preload && route.options.preload === false) {
2738
- // Do nothing
2739
- } else if (loaderShouldRunAsync && !sync) {
2740
- loaderIsRunningAsync = true
2741
- ;(async () => {
2742
- try {
2743
- await runLoader()
2744
- const match = this.getMatch(matchId)!
2745
- match._nonReactive.loaderPromise?.resolve()
2746
- match._nonReactive.loadPromise?.resolve()
2747
- match._nonReactive.loaderPromise = undefined
2748
- } catch (err) {
2749
- if (isRedirect(err)) {
2750
- await this.navigate(err.options)
2751
- }
2752
- }
2753
- })()
2754
- } else if (
2755
- status !== 'success' ||
2756
- (loaderShouldRunAsync && sync)
2757
- ) {
2758
- await runLoader()
2759
- } else {
2760
- // if the loader did not run, still update head.
2761
- // reason: parent's beforeLoad may have changed the route context
2762
- // and only now do we know the route context (and that the loader would not run)
2763
- const headResult = executeHead()
2764
- if (headResult) {
2765
- const head = await headResult
2766
- updateMatch(matchId, (prev) => ({
2767
- ...prev,
2768
- ...head,
2769
- }))
2770
- }
2771
- }
2772
- }
2773
- if (!loaderIsRunningAsync) {
2774
- const match = this.getMatch(matchId)!
2775
- match._nonReactive.loaderPromise?.resolve()
2776
- match._nonReactive.loadPromise?.resolve()
2777
- }
2778
-
2779
- updateMatch(matchId, (prev) => {
2780
- clearTimeout(prev._nonReactive.pendingTimeout)
2781
- prev._nonReactive.pendingTimeout = undefined
2782
- if (!loaderIsRunningAsync)
2783
- prev._nonReactive.loaderPromise = undefined
2784
- prev._nonReactive.dehydrated = undefined
2785
- return {
2786
- ...prev,
2787
- isFetching: loaderIsRunningAsync
2788
- ? prev.isFetching
2789
- : false,
2790
- invalid: false,
2791
- }
2792
- })
2793
- return this.getMatch(matchId)!
2794
- })(),
2909
+ try {
2910
+ await new Promise<void>((resolveAll, rejectAll) => {
2911
+ ;(async () => {
2912
+ try {
2913
+ // Execute all beforeLoads one by one
2914
+ for (let i = 0; i < innerLoadContext.matches.length; i++) {
2915
+ const beforeLoad = this.handleBeforeLoad(innerLoadContext, i)
2916
+ if (isPromise(beforeLoad)) await beforeLoad
2917
+ }
2918
+
2919
+ // Execute all loaders in parallel
2920
+ const max =
2921
+ innerLoadContext.firstBadMatchIndex ??
2922
+ innerLoadContext.matches.length
2923
+ for (let i = 0; i < max; i++) {
2924
+ innerLoadContext.matchPromises.push(
2925
+ this.loadRouteMatch(innerLoadContext, i),
2795
2926
  )
2796
- })
2927
+ }
2797
2928
 
2798
- await Promise.all(matchPromises)
2929
+ await Promise.all(innerLoadContext.matchPromises)
2799
2930
 
2800
2931
  resolveAll()
2801
2932
  } catch (err) {
@@ -2803,18 +2934,20 @@ export class RouterCore<
2803
2934
  }
2804
2935
  })()
2805
2936
  })
2806
- await triggerOnReady()
2937
+ const readyPromise = this.triggerOnReady(innerLoadContext)
2938
+ if (isPromise(readyPromise)) await readyPromise
2807
2939
  } catch (err) {
2808
- if (isRedirect(err) || isNotFound(err)) {
2809
- if (isNotFound(err) && !allPreload) {
2810
- await triggerOnReady()
2811
- }
2812
-
2940
+ if (isNotFound(err) && !innerLoadContext.preload) {
2941
+ const readyPromise = this.triggerOnReady(innerLoadContext)
2942
+ if (isPromise(readyPromise)) await readyPromise
2943
+ throw err
2944
+ }
2945
+ if (isRedirect(err)) {
2813
2946
  throw err
2814
2947
  }
2815
2948
  }
2816
2949
 
2817
- return matches
2950
+ return innerLoadContext.matches
2818
2951
  }
2819
2952
 
2820
2953
  invalidate: InvalidateFn<
@@ -3088,13 +3221,9 @@ export class RouterCore<
3088
3221
 
3089
3222
  serverSsr?: ServerSsr
3090
3223
 
3091
- _handleNotFound = (
3092
- matches: Array<AnyRouteMatch>,
3224
+ private _handleNotFound = (
3225
+ innerLoadContext: InnerLoadContext,
3093
3226
  err: NotFoundError,
3094
- updateMatch: (
3095
- id: string,
3096
- updater: (match: AnyRouteMatch) => AnyRouteMatch,
3097
- ) => void = this.updateMatch,
3098
3227
  ) => {
3099
3228
  // Find the route that should handle the not found error
3100
3229
  // First check if a specific route is requested to show the error
@@ -3102,7 +3231,7 @@ export class RouterCore<
3102
3231
  const matchesByRouteId: Record<string, AnyRouteMatch> = {}
3103
3232
 
3104
3233
  // Setup routesByRouteId object for quick access
3105
- for (const match of matches) {
3234
+ for (const match of innerLoadContext.matches) {
3106
3235
  matchesByRouteId[match.routeId] = match
3107
3236
  }
3108
3237
 
@@ -3131,7 +3260,7 @@ export class RouterCore<
3131
3260
  )
3132
3261
 
3133
3262
  // Assign the error to the match - using non-null assertion since we've checked with invariant
3134
- updateMatch(matchForRoute.id, (prev) => ({
3263
+ innerLoadContext.updateMatch(matchForRoute.id, (prev) => ({
3135
3264
  ...prev,
3136
3265
  status: 'notFound',
3137
3266
  error: err,
@@ -3140,7 +3269,7 @@ export class RouterCore<
3140
3269
 
3141
3270
  if ((err as any).routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute) {
3142
3271
  err.routeId = routeCursor.parentRoute.id
3143
- this._handleNotFound(matches, err, updateMatch)
3272
+ this._handleNotFound(innerLoadContext, err)
3144
3273
  }
3145
3274
  }
3146
3275
 
@@ -3155,6 +3284,16 @@ export class SearchParamError extends Error {}
3155
3284
 
3156
3285
  export class PathParamError extends Error {}
3157
3286
 
3287
+ function makeMaybe<TValue, TError>(
3288
+ value: TValue,
3289
+ error: TError,
3290
+ ): { status: 'success'; value: TValue } | { status: 'error'; error: TError } {
3291
+ if (error) {
3292
+ return { status: 'error' as const, error }
3293
+ }
3294
+ return { status: 'success' as const, value }
3295
+ }
3296
+
3158
3297
  const normalize = (str: string) =>
3159
3298
  str.endsWith('/') && str.length > 1 ? str.slice(0, -1) : str
3160
3299
  function comparePaths(a: string, b: string) {