@tanstack/router-core 1.131.14 → 1.131.17

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,856 @@ 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
+ // Update the match and prematurely resolve the loadMatches promise so that
2322
+ // the pending component can start rendering
2323
+ this.triggerOnReady(innerLoadContext)
2324
+ }, pendingMs)
2325
+ match._nonReactive.pendingTimeout = pendingTimeout
2326
+ }
2327
+ }
2328
+
2329
+ private shouldExecuteBeforeLoad = (
2330
+ innerLoadContext: InnerLoadContext,
2331
+ matchId: string,
2332
+ route: AnyRoute,
2333
+ ): boolean | Promise<boolean> => {
2334
+ const existingMatch = this.getMatch(matchId)!
2335
+
2336
+ // If we are in the middle of a load, either of these will be present
2337
+ // (not to be confused with `loadPromise`, which is always defined)
2338
+ if (
2339
+ !existingMatch._nonReactive.beforeLoadPromise &&
2340
+ !existingMatch._nonReactive.loaderPromise
2341
+ )
2342
+ return true
2343
+
2344
+ this.setupPendingTimeout(innerLoadContext, matchId, route)
2345
+
2346
+ const then = () => {
2347
+ let shouldExecuteBeforeLoad = true
2348
+ const match = this.getMatch(matchId)!
2349
+ if (match.status === 'error') {
2350
+ shouldExecuteBeforeLoad = true
2351
+ } else if (
2352
+ match.preload &&
2353
+ (match.status === 'redirected' || match.status === 'notFound')
2135
2354
  ) {
2136
- throw err
2355
+ this.handleRedirectAndNotFound(innerLoadContext, match, match.error)
2137
2356
  }
2357
+ return shouldExecuteBeforeLoad
2358
+ }
2138
2359
 
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
2360
+ // Wait for the beforeLoad to resolve before we continue
2361
+ return existingMatch._nonReactive.beforeLoadPromise
2362
+ ? existingMatch._nonReactive.beforeLoadPromise.then(then)
2363
+ : then()
2364
+ }
2365
+
2366
+ private executeBeforeLoad = (
2367
+ innerLoadContext: InnerLoadContext,
2368
+ matchId: string,
2369
+ index: number,
2370
+ route: AnyRoute,
2371
+ ): void | Promise<void> => {
2372
+ const match = this.getMatch(matchId)!
2373
+
2374
+ match._nonReactive.beforeLoadPromise = createControlledPromise<void>()
2375
+ // explicitly capture the previous loadPromise
2376
+ const prevLoadPromise = match._nonReactive.loadPromise
2377
+ match._nonReactive.loadPromise = createControlledPromise<void>(() => {
2378
+ prevLoadPromise?.resolve()
2379
+ })
2380
+
2381
+ const { paramsError, searchError } = match
2382
+
2383
+ if (paramsError) {
2384
+ this.handleSerialError(
2385
+ innerLoadContext,
2386
+ index,
2387
+ paramsError,
2388
+ 'PARSE_PARAMS',
2389
+ )
2390
+ }
2391
+
2392
+ if (searchError) {
2393
+ this.handleSerialError(
2394
+ innerLoadContext,
2395
+ index,
2396
+ searchError,
2397
+ 'VALIDATE_SEARCH',
2398
+ )
2399
+ }
2400
+
2401
+ this.setupPendingTimeout(innerLoadContext, matchId, route)
2402
+
2403
+ const abortController = new AbortController()
2404
+
2405
+ const parentMatchId = innerLoadContext.matches[index - 1]?.id
2406
+ const parentMatch = parentMatchId
2407
+ ? this.getMatch(parentMatchId)!
2408
+ : undefined
2409
+ const parentMatchContext =
2410
+ parentMatch?.context ?? this.options.context ?? undefined
2411
+
2412
+ const context = { ...parentMatchContext, ...match.__routeContext }
2413
+
2414
+ let isPending = false
2415
+ const pending = () => {
2416
+ if (isPending) return
2417
+ isPending = true
2418
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2419
+ ...prev,
2420
+ isFetching: 'beforeLoad',
2421
+ fetchCount: prev.fetchCount + 1,
2422
+ abortController,
2423
+ context,
2424
+ }))
2425
+ }
2426
+
2427
+ const resolve = () => {
2428
+ match._nonReactive.beforeLoadPromise?.resolve()
2429
+ match._nonReactive.beforeLoadPromise = undefined
2430
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2431
+ ...prev,
2432
+ isFetching: false,
2433
+ }))
2434
+ }
2435
+
2436
+ // if there is no `beforeLoad` option, skip everything, batch update the store, return early
2437
+ if (!route.options.beforeLoad) {
2438
+ batch(() => {
2439
+ pending()
2440
+ resolve()
2441
+ })
2442
+ return
2443
+ }
2145
2444
 
2146
- const status = isRedirect(err) ? 'redirected' : 'notFound'
2445
+ const { search, params, cause } = match
2446
+ const preload = this.resolvePreload(innerLoadContext, matchId)
2447
+ const beforeLoadFnContext: BeforeLoadContextOptions<
2448
+ any,
2449
+ any,
2450
+ any,
2451
+ any,
2452
+ any
2453
+ > = {
2454
+ search,
2455
+ abortController,
2456
+ params,
2457
+ preload,
2458
+ context,
2459
+ location: innerLoadContext.location,
2460
+ navigate: (opts: any) =>
2461
+ this.navigate({ ...opts, _fromLocation: innerLoadContext.location }),
2462
+ buildLocation: this.buildLocation,
2463
+ cause: preload ? 'preload' : cause,
2464
+ matches: innerLoadContext.matches,
2465
+ }
2147
2466
 
2148
- updateMatch(match.id, (prev) => ({
2467
+ const updateContext = (beforeLoadContext: any) => {
2468
+ if (beforeLoadContext === undefined) {
2469
+ batch(() => {
2470
+ pending()
2471
+ resolve()
2472
+ })
2473
+ return
2474
+ }
2475
+ if (isRedirect(beforeLoadContext) || isNotFound(beforeLoadContext)) {
2476
+ pending()
2477
+ this.handleSerialError(
2478
+ innerLoadContext,
2479
+ index,
2480
+ beforeLoadContext,
2481
+ 'BEFORE_LOAD',
2482
+ )
2483
+ }
2484
+
2485
+ batch(() => {
2486
+ pending()
2487
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2149
2488
  ...prev,
2150
- status,
2151
- isFetching: false,
2152
- error: err,
2489
+ __beforeLoadContext: beforeLoadContext,
2490
+ context: {
2491
+ ...prev.context,
2492
+ ...beforeLoadContext,
2493
+ },
2153
2494
  }))
2495
+ resolve()
2496
+ })
2497
+ }
2154
2498
 
2155
- if (isNotFound(err) && !err.routeId) {
2156
- err.routeId = match.routeId
2157
- }
2158
-
2159
- match._nonReactive.loadPromise?.resolve()
2499
+ let beforeLoadContext
2500
+ try {
2501
+ beforeLoadContext = route.options.beforeLoad(beforeLoadFnContext)
2502
+ if (isPromise(beforeLoadContext)) {
2503
+ pending()
2504
+ return beforeLoadContext
2505
+ .catch((err) => {
2506
+ this.handleSerialError(innerLoadContext, index, err, 'BEFORE_LOAD')
2507
+ })
2508
+ .then(updateContext)
2160
2509
  }
2510
+ } catch (err) {
2511
+ pending()
2512
+ this.handleSerialError(innerLoadContext, index, err, 'BEFORE_LOAD')
2513
+ }
2161
2514
 
2162
- if (isRedirect(err)) {
2163
- rendered = true
2164
- err.options._fromLocation = location
2165
- err.redirectHandled = true
2166
- err = this.resolveRedirect(err)
2167
- throw err
2168
- } else {
2169
- this._handleNotFound(matches, err, updateMatch)
2170
- throw err
2515
+ updateContext(beforeLoadContext)
2516
+ return
2517
+ }
2518
+
2519
+ private handleBeforeLoad = (
2520
+ innerLoadContext: InnerLoadContext,
2521
+ index: number,
2522
+ ): void | Promise<void> => {
2523
+ const { id: matchId, routeId } = innerLoadContext.matches[index]!
2524
+ const route = this.looseRoutesById[routeId]!
2525
+
2526
+ const serverSsr = () => {
2527
+ // on the server, determine whether SSR the current match or not
2528
+ if (this.isServer) {
2529
+ const maybePromise = this.isBeforeLoadSsr(
2530
+ innerLoadContext,
2531
+ matchId,
2532
+ index,
2533
+ route,
2534
+ )
2535
+ if (isPromise(maybePromise)) return maybePromise.then(queueExecution)
2171
2536
  }
2537
+ return queueExecution()
2172
2538
  }
2173
2539
 
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
2540
+ const queueExecution = () => {
2541
+ if (this.shouldSkipLoader(matchId)) return
2542
+ const shouldExecuteBeforeLoadResult = this.shouldExecuteBeforeLoad(
2543
+ innerLoadContext,
2544
+ matchId,
2545
+ route,
2546
+ )
2547
+ return isPromise(shouldExecuteBeforeLoadResult)
2548
+ ? shouldExecuteBeforeLoadResult.then(execute)
2549
+ : execute(shouldExecuteBeforeLoadResult)
2550
+ }
2551
+
2552
+ const execute = (shouldExecuteBeforeLoad: boolean) => {
2553
+ if (shouldExecuteBeforeLoad) {
2554
+ // If we are not in the middle of a load OR the previous load failed, start it
2555
+ return this.executeBeforeLoad(innerLoadContext, matchId, index, route)
2179
2556
  }
2557
+ return
2558
+ }
2180
2559
 
2181
- if (this.isServer) {
2182
- if (match.ssr === false) {
2183
- return true
2184
- }
2560
+ return serverSsr()
2561
+ }
2562
+
2563
+ private executeHead = (
2564
+ innerLoadContext: InnerLoadContext,
2565
+ matchId: string,
2566
+ route: AnyRoute,
2567
+ ): void | Promise<
2568
+ Pick<
2569
+ AnyRouteMatch,
2570
+ 'meta' | 'links' | 'headScripts' | 'headers' | 'scripts' | 'styles'
2571
+ >
2572
+ > => {
2573
+ const match = this.getMatch(matchId)
2574
+ // in case of a redirecting match during preload, the match does not exist
2575
+ if (!match) {
2576
+ return
2577
+ }
2578
+ if (
2579
+ !route.options.head &&
2580
+ !route.options.scripts &&
2581
+ !route.options.headers
2582
+ ) {
2583
+ return
2584
+ }
2585
+ const assetContext = {
2586
+ matches: innerLoadContext.matches,
2587
+ match,
2588
+ params: match.params,
2589
+ loaderData: match.loaderData,
2590
+ }
2591
+
2592
+ return Promise.all([
2593
+ route.options.head?.(assetContext),
2594
+ route.options.scripts?.(assetContext),
2595
+ route.options.headers?.(assetContext),
2596
+ ]).then(([headFnContent, scripts, headers]) => {
2597
+ const meta = headFnContent?.meta
2598
+ const links = headFnContent?.links
2599
+ const headScripts = headFnContent?.scripts
2600
+ const styles = headFnContent?.styles
2601
+
2602
+ return {
2603
+ meta,
2604
+ links,
2605
+ headScripts,
2606
+ headers,
2607
+ scripts,
2608
+ styles,
2185
2609
  }
2186
- return false
2610
+ })
2611
+ }
2612
+
2613
+ private potentialPendingMinPromise = (
2614
+ matchId: string,
2615
+ ): void | ControlledPromise<void> => {
2616
+ const latestMatch = this.getMatch(matchId)!
2617
+ return latestMatch._nonReactive.minPendingPromise
2618
+ }
2619
+
2620
+ private getLoaderContext = (
2621
+ innerLoadContext: InnerLoadContext,
2622
+ matchId: string,
2623
+ index: number,
2624
+ route: AnyRoute,
2625
+ ): LoaderFnContext => {
2626
+ const parentMatchPromise = innerLoadContext.matchPromises[index - 1] as any
2627
+ const { params, loaderDeps, abortController, context, cause } =
2628
+ this.getMatch(matchId)!
2629
+
2630
+ const preload = this.resolvePreload(innerLoadContext, matchId)
2631
+
2632
+ return {
2633
+ params,
2634
+ deps: loaderDeps,
2635
+ preload: !!preload,
2636
+ parentMatchPromise,
2637
+ abortController: abortController,
2638
+ context,
2639
+ location: innerLoadContext.location,
2640
+ navigate: (opts) =>
2641
+ this.navigate({ ...opts, _fromLocation: innerLoadContext.location }),
2642
+ cause: preload ? 'preload' : cause,
2643
+ route,
2187
2644
  }
2645
+ }
2188
2646
 
2647
+ private runLoader = async (
2648
+ innerLoadContext: InnerLoadContext,
2649
+ matchId: string,
2650
+ index: number,
2651
+ route: AnyRoute,
2652
+ ): Promise<void> => {
2189
2653
  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
- }
2654
+ // If the Matches component rendered
2655
+ // the pending component and needs to show it for
2656
+ // a minimum duration, we''ll wait for it to resolve
2657
+ // before committing to the match and resolving
2658
+ // the loadPromise
2207
2659
 
2208
- err.routerCode = routerCode
2209
- firstBadMatchIndex = firstBadMatchIndex ?? index
2210
- handleRedirectAndNotFound(this.getMatch(matchId), err)
2660
+ // Actually run the loader and handle the result
2661
+ try {
2662
+ if (!this.isServer || this.getMatch(matchId)!.ssr === true) {
2663
+ this.loadRouteChunk(route)
2664
+ }
2211
2665
 
2212
- try {
2213
- route.options.onError?.(err)
2214
- } catch (errorHandlerErr) {
2215
- err = errorHandlerErr
2216
- handleRedirectAndNotFound(this.getMatch(matchId), err)
2217
- }
2666
+ // Kick off the loader!
2667
+ const loaderResult = route.options.loader?.(
2668
+ this.getLoaderContext(innerLoadContext, matchId, index, route),
2669
+ )
2670
+ const loaderResultIsPromise =
2671
+ route.options.loader && isPromise(loaderResult)
2672
+
2673
+ const willLoadSomething = !!(
2674
+ loaderResultIsPromise ||
2675
+ route._lazyPromise ||
2676
+ route._componentsPromise ||
2677
+ route.options.head ||
2678
+ route.options.scripts ||
2679
+ route.options.headers ||
2680
+ this.getMatch(matchId)!._nonReactive.minPendingPromise
2681
+ )
2218
2682
 
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
- }
2683
+ if (willLoadSomething) {
2684
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2685
+ ...prev,
2686
+ isFetching: 'loader',
2687
+ }))
2688
+ }
2234
2689
 
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
- }
2690
+ if (route.options.loader) {
2691
+ const loaderData = loaderResultIsPromise
2692
+ ? await loaderResult
2693
+ : loaderResult
2292
2694
 
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
- }
2695
+ this.handleRedirectAndNotFound(
2696
+ innerLoadContext,
2697
+ this.getMatch(matchId),
2698
+ loaderData,
2699
+ )
2700
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2701
+ ...prev,
2702
+ loaderData,
2703
+ }))
2704
+ }
2302
2705
 
2303
- if (shouldSkipLoader(matchId)) {
2304
- continue
2305
- }
2706
+ // Lazy option can modify the route options,
2707
+ // so we need to wait for it to resolve before
2708
+ // we can use the options
2709
+ if (route._lazyPromise) await route._lazyPromise
2710
+ const headResult = this.executeHead(innerLoadContext, matchId, route)
2711
+ const head = headResult ? await headResult : undefined
2712
+ const pendingPromise = this.potentialPendingMinPromise(matchId)
2713
+ if (pendingPromise) await pendingPromise
2714
+
2715
+ // Last but not least, wait for the the components
2716
+ // to be preloaded before we resolve the match
2717
+ if (route._componentsPromise) await route._componentsPromise
2718
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2719
+ ...prev,
2720
+ error: undefined,
2721
+ status: 'success',
2722
+ isFetching: false,
2723
+ updatedAt: Date.now(),
2724
+ ...head,
2725
+ }))
2726
+ } catch (e) {
2727
+ let error = e
2306
2728
 
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
- )
2729
+ const pendingPromise = this.potentialPendingMinPromise(matchId)
2730
+ if (pendingPromise) await pendingPromise
2319
2731
 
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
- }
2732
+ this.handleRedirectAndNotFound(
2733
+ innerLoadContext,
2734
+ this.getMatch(matchId),
2735
+ e,
2736
+ )
2737
+
2738
+ try {
2739
+ route.options.onError?.(e)
2740
+ } catch (onErrorError) {
2741
+ error = onErrorError
2742
+ this.handleRedirectAndNotFound(
2743
+ innerLoadContext,
2744
+ this.getMatch(matchId),
2745
+ onErrorError,
2746
+ )
2747
+ }
2748
+ const headResult = this.executeHead(innerLoadContext, matchId, route)
2749
+ const head = headResult ? await headResult : undefined
2750
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2751
+ ...prev,
2752
+ error,
2753
+ status: 'error',
2754
+ isFetching: false,
2755
+ ...head,
2756
+ }))
2757
+ }
2758
+ } catch (err) {
2759
+ const match = this.getMatch(matchId)
2760
+ // in case of a redirecting match during preload, the match does not exist
2761
+ if (match) {
2762
+ const headResult = this.executeHead(innerLoadContext, matchId, route)
2763
+ if (headResult) {
2764
+ const head = await headResult
2765
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2766
+ ...prev,
2767
+ ...head,
2768
+ }))
2769
+ }
2770
+ match._nonReactive.loaderPromise = undefined
2771
+ }
2772
+ this.handleRedirectAndNotFound(innerLoadContext, match, err)
2773
+ }
2774
+ }
2775
+
2776
+ private loadRouteMatch = async (
2777
+ innerLoadContext: InnerLoadContext,
2778
+ index: number,
2779
+ ): Promise<AnyRouteMatch> => {
2780
+ const { id: matchId, routeId } = innerLoadContext.matches[index]!
2781
+ let loaderShouldRunAsync = false
2782
+ let loaderIsRunningAsync = false
2783
+ const route = this.looseRoutesById[routeId]!
2784
+
2785
+ const prevMatch = this.getMatch(matchId)!
2786
+ if (this.shouldSkipLoader(matchId)) {
2787
+ if (this.isServer) {
2788
+ const headResult = this.executeHead(innerLoadContext, matchId, route)
2789
+ if (headResult) {
2790
+ const head = await headResult
2791
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2792
+ ...prev,
2793
+ ...head,
2794
+ }))
2795
+ }
2796
+ return this.getMatch(matchId)!
2797
+ }
2798
+ }
2799
+ // there is a loaderPromise, so we are in the middle of a load
2800
+ else if (prevMatch._nonReactive.loaderPromise) {
2801
+ // do not block if we already have stale data we can show
2802
+ // but only if the ongoing load is not a preload since error handling is different for preloads
2803
+ // and we don't want to swallow errors
2804
+ if (
2805
+ prevMatch.status === 'success' &&
2806
+ !innerLoadContext.sync &&
2807
+ !prevMatch.preload
2808
+ ) {
2809
+ return this.getMatch(matchId)!
2810
+ }
2811
+ await prevMatch._nonReactive.loaderPromise
2812
+ const match = this.getMatch(matchId)!
2813
+ if (match.error) {
2814
+ this.handleRedirectAndNotFound(innerLoadContext, match, match.error)
2815
+ }
2816
+ } else {
2817
+ // This is where all of the stale-while-revalidate magic happens
2818
+ const age = Date.now() - this.getMatch(matchId)!.updatedAt
2819
+
2820
+ const preload = this.resolvePreload(innerLoadContext, matchId)
2821
+
2822
+ const staleAge = preload
2823
+ ? (route.options.preloadStaleTime ??
2824
+ this.options.defaultPreloadStaleTime ??
2825
+ 30_000) // 30 seconds for preloads by default
2826
+ : (route.options.staleTime ?? this.options.defaultStaleTime ?? 0)
2827
+
2828
+ const shouldReloadOption = route.options.shouldReload
2829
+
2830
+ // Default to reloading the route all the time
2831
+ // Allow shouldReload to get the last say,
2832
+ // if provided.
2833
+ const shouldReload =
2834
+ typeof shouldReloadOption === 'function'
2835
+ ? shouldReloadOption(
2836
+ this.getLoaderContext(innerLoadContext, matchId, index, route),
2837
+ )
2838
+ : shouldReloadOption
2839
+
2840
+ innerLoadContext.updateMatch(matchId, (prev) => {
2841
+ prev._nonReactive.loaderPromise = createControlledPromise<void>()
2842
+ return {
2843
+ ...prev,
2844
+ preload:
2845
+ !!preload && !this.state.matches.some((d) => d.id === matchId),
2846
+ }
2847
+ })
2848
+
2849
+ // If the route is successful and still fresh, just resolve
2850
+ const { status, invalid } = this.getMatch(matchId)!
2851
+ loaderShouldRunAsync =
2852
+ status === 'success' && (invalid || (shouldReload ?? age > staleAge))
2853
+ if (preload && route.options.preload === false) {
2854
+ // Do nothing
2855
+ } else if (loaderShouldRunAsync && !innerLoadContext.sync) {
2856
+ loaderIsRunningAsync = true
2857
+ ;(async () => {
2858
+ try {
2859
+ await this.runLoader(innerLoadContext, matchId, index, route)
2860
+ const match = this.getMatch(matchId)!
2861
+ match._nonReactive.loaderPromise?.resolve()
2862
+ match._nonReactive.loadPromise?.resolve()
2863
+ match._nonReactive.loaderPromise = undefined
2864
+ } catch (err) {
2865
+ if (isRedirect(err)) {
2866
+ await this.navigate(err.options)
2459
2867
  }
2868
+ }
2869
+ })()
2870
+ } else if (
2871
+ status !== 'success' ||
2872
+ (loaderShouldRunAsync && innerLoadContext.sync)
2873
+ ) {
2874
+ await this.runLoader(innerLoadContext, matchId, index, route)
2875
+ } else {
2876
+ // if the loader did not run, still update head.
2877
+ // reason: parent's beforeLoad may have changed the route context
2878
+ // and only now do we know the route context (and that the loader would not run)
2879
+ const headResult = this.executeHead(innerLoadContext, matchId, route)
2880
+ if (headResult) {
2881
+ const head = await headResult
2882
+ innerLoadContext.updateMatch(matchId, (prev) => ({
2883
+ ...prev,
2884
+ ...head,
2885
+ }))
2886
+ }
2887
+ }
2888
+ }
2889
+ if (!loaderIsRunningAsync) {
2890
+ const match = this.getMatch(matchId)!
2891
+ match._nonReactive.loaderPromise?.resolve()
2892
+ match._nonReactive.loadPromise?.resolve()
2893
+ }
2460
2894
 
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
- }
2895
+ innerLoadContext.updateMatch(matchId, (prev) => {
2896
+ clearTimeout(prev._nonReactive.pendingTimeout)
2897
+ prev._nonReactive.pendingTimeout = undefined
2898
+ if (!loaderIsRunningAsync) prev._nonReactive.loaderPromise = undefined
2899
+ prev._nonReactive.dehydrated = undefined
2900
+ return {
2901
+ ...prev,
2902
+ isFetching: loaderIsRunningAsync ? prev.isFetching : false,
2903
+ invalid: false,
2904
+ }
2905
+ })
2906
+ return this.getMatch(matchId)!
2907
+ }
2490
2908
 
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
- }
2909
+ loadMatches = async (baseContext: {
2910
+ location: ParsedLocation
2911
+ matches: Array<AnyRouteMatch>
2912
+ preload?: boolean
2913
+ onReady?: () => Promise<void>
2914
+ updateMatch?: UpdateMatchFn
2915
+ sync?: boolean
2916
+ }): Promise<Array<MakeRouteMatch>> => {
2917
+ const innerLoadContext = baseContext as InnerLoadContext
2918
+ innerLoadContext.updateMatch ??= this.updateMatch
2919
+ innerLoadContext.matchPromises = []
2576
2920
 
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
- }
2921
+ // make sure the pending component is immediately rendered when hydrating a match that is not SSRed
2922
+ // the pending component was already rendered on the server and we want to keep it shown on the client until minPendingMs is reached
2923
+ if (!this.isServer && this.state.matches.some((d) => d._forcePending)) {
2924
+ this.triggerOnReady(innerLoadContext)
2925
+ }
2731
2926
 
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
- })(),
2927
+ try {
2928
+ await new Promise<void>((resolveAll, rejectAll) => {
2929
+ ;(async () => {
2930
+ try {
2931
+ // Execute all beforeLoads one by one
2932
+ for (let i = 0; i < innerLoadContext.matches.length; i++) {
2933
+ const beforeLoad = this.handleBeforeLoad(innerLoadContext, i)
2934
+ if (isPromise(beforeLoad)) await beforeLoad
2935
+ }
2936
+
2937
+ // Execute all loaders in parallel
2938
+ const max =
2939
+ innerLoadContext.firstBadMatchIndex ??
2940
+ innerLoadContext.matches.length
2941
+ for (let i = 0; i < max; i++) {
2942
+ innerLoadContext.matchPromises.push(
2943
+ this.loadRouteMatch(innerLoadContext, i),
2795
2944
  )
2796
- })
2945
+ }
2797
2946
 
2798
- await Promise.all(matchPromises)
2947
+ await Promise.all(innerLoadContext.matchPromises)
2799
2948
 
2800
2949
  resolveAll()
2801
2950
  } catch (err) {
@@ -2803,18 +2952,20 @@ export class RouterCore<
2803
2952
  }
2804
2953
  })()
2805
2954
  })
2806
- await triggerOnReady()
2955
+ const readyPromise = this.triggerOnReady(innerLoadContext)
2956
+ if (isPromise(readyPromise)) await readyPromise
2807
2957
  } catch (err) {
2808
- if (isRedirect(err) || isNotFound(err)) {
2809
- if (isNotFound(err) && !allPreload) {
2810
- await triggerOnReady()
2811
- }
2812
-
2958
+ if (isNotFound(err) && !innerLoadContext.preload) {
2959
+ const readyPromise = this.triggerOnReady(innerLoadContext)
2960
+ if (isPromise(readyPromise)) await readyPromise
2961
+ throw err
2962
+ }
2963
+ if (isRedirect(err)) {
2813
2964
  throw err
2814
2965
  }
2815
2966
  }
2816
2967
 
2817
- return matches
2968
+ return innerLoadContext.matches
2818
2969
  }
2819
2970
 
2820
2971
  invalidate: InvalidateFn<
@@ -3088,13 +3239,9 @@ export class RouterCore<
3088
3239
 
3089
3240
  serverSsr?: ServerSsr
3090
3241
 
3091
- _handleNotFound = (
3092
- matches: Array<AnyRouteMatch>,
3242
+ private _handleNotFound = (
3243
+ innerLoadContext: InnerLoadContext,
3093
3244
  err: NotFoundError,
3094
- updateMatch: (
3095
- id: string,
3096
- updater: (match: AnyRouteMatch) => AnyRouteMatch,
3097
- ) => void = this.updateMatch,
3098
3245
  ) => {
3099
3246
  // Find the route that should handle the not found error
3100
3247
  // First check if a specific route is requested to show the error
@@ -3102,7 +3249,7 @@ export class RouterCore<
3102
3249
  const matchesByRouteId: Record<string, AnyRouteMatch> = {}
3103
3250
 
3104
3251
  // Setup routesByRouteId object for quick access
3105
- for (const match of matches) {
3252
+ for (const match of innerLoadContext.matches) {
3106
3253
  matchesByRouteId[match.routeId] = match
3107
3254
  }
3108
3255
 
@@ -3131,7 +3278,7 @@ export class RouterCore<
3131
3278
  )
3132
3279
 
3133
3280
  // Assign the error to the match - using non-null assertion since we've checked with invariant
3134
- updateMatch(matchForRoute.id, (prev) => ({
3281
+ innerLoadContext.updateMatch(matchForRoute.id, (prev) => ({
3135
3282
  ...prev,
3136
3283
  status: 'notFound',
3137
3284
  error: err,
@@ -3140,7 +3287,7 @@ export class RouterCore<
3140
3287
 
3141
3288
  if ((err as any).routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute) {
3142
3289
  err.routeId = routeCursor.parentRoute.id
3143
- this._handleNotFound(matches, err, updateMatch)
3290
+ this._handleNotFound(innerLoadContext, err)
3144
3291
  }
3145
3292
  }
3146
3293
 
@@ -3155,6 +3302,16 @@ export class SearchParamError extends Error {}
3155
3302
 
3156
3303
  export class PathParamError extends Error {}
3157
3304
 
3305
+ function makeMaybe<TValue, TError>(
3306
+ value: TValue,
3307
+ error: TError,
3308
+ ): { status: 'success'; value: TValue } | { status: 'error'; error: TError } {
3309
+ if (error) {
3310
+ return { status: 'error' as const, error }
3311
+ }
3312
+ return { status: 'success' as const, value }
3313
+ }
3314
+
3158
3315
  const normalize = (str: string) =>
3159
3316
  str.endsWith('/') && str.length > 1 ? str.slice(0, -1) : str
3160
3317
  function comparePaths(a: string, b: string) {