From 85f379cbc3973a4586952ebb582d1b922b48ede9 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 23 Aug 2025 13:13:39 +0200 Subject: [PATCH 1/8] refactor(router-core): flatten loadRouteMatch, can run synchronously --- packages/router-core/src/load-matches.ts | 235 +++++++++++++---------- packages/router-core/src/route.ts | 2 +- 2 files changed, 130 insertions(+), 107 deletions(-) diff --git a/packages/router-core/src/load-matches.ts b/packages/router-core/src/load-matches.ts index c1edd5648f..72c1709618 100644 --- a/packages/router-core/src/load-matches.ts +++ b/packages/router-core/src/load-matches.ts @@ -4,6 +4,7 @@ import { createControlledPromise, isPromise } from './utils' import { isNotFound } from './not-found' import { rootRouteId } from './root' import { isRedirect } from './redirect' +import type { Awaitable } from './utils' import type { NotFoundError } from './not-found' import type { ParsedLocation } from './location' import type { @@ -34,7 +35,7 @@ type InnerLoadContext = { onReady?: () => Promise sync?: boolean /** mutable state, scoped to a `loadMatches` call */ - matchPromises: Array> + matchPromises: Array> } const triggerOnReady = (inner: InnerLoadContext): void | Promise => { @@ -703,10 +704,10 @@ const runLoader = async ( } } -const loadRouteMatch = async ( +const loadRouteMatch = ( inner: InnerLoadContext, index: number, -): Promise => { +): Awaitable => { const { id: matchId, routeId } = inner.matches[index]! let loaderShouldRunAsync = false let loaderIsRunningAsync = false @@ -716,121 +717,140 @@ const loadRouteMatch = async ( if (inner.router.isServer) { const headResult = executeHead(inner, matchId, route) if (headResult) { - const head = await headResult - inner.updateMatch(matchId, (prev) => ({ - ...prev, - ...head, - })) + return headResult.then((head) => { + inner.updateMatch(matchId, (prev) => ({ + ...prev, + ...head, + })) + return inner.router.getMatch(matchId)! + }) } return inner.router.getMatch(matchId)! } - } else { - const prevMatch = inner.router.getMatch(matchId)! - // there is a loaderPromise, so we are in the middle of a load - if (prevMatch._nonReactive.loaderPromise) { - // do not block if we already have stale data we can show - // but only if the ongoing load is not a preload since error handling is different for preloads - // and we don't want to swallow errors - if (prevMatch.status === 'success' && !inner.sync && !prevMatch.preload) { - return prevMatch - } - await prevMatch._nonReactive.loaderPromise + return settleLoadRouteMatch() + } + + const prevMatch = inner.router.getMatch(matchId)! + + // there is a loaderPromise, so we are in the middle of a load + if (prevMatch._nonReactive.loaderPromise) { + // do not block if we already have stale data we can show + // but only if the ongoing load is not a preload since error handling is different for preloads + // and we don't want to swallow errors + if (prevMatch.status === 'success' && !inner.sync && !prevMatch.preload) { + return prevMatch + } + return prevMatch._nonReactive.loaderPromise.then(() => { const match = inner.router.getMatch(matchId)! if (match.error) { handleRedirectAndNotFound(inner, match, match.error) } - } else { - // This is where all of the stale-while-revalidate magic happens - const age = Date.now() - prevMatch.updatedAt - - const preload = resolvePreload(inner, matchId) - - const staleAge = preload - ? (route.options.preloadStaleTime ?? - inner.router.options.defaultPreloadStaleTime ?? - 30_000) // 30 seconds for preloads by default - : (route.options.staleTime ?? - inner.router.options.defaultStaleTime ?? - 0) - - const shouldReloadOption = route.options.shouldReload - - // Default to reloading the route all the time - // Allow shouldReload to get the last say, - // if provided. - const shouldReload = - typeof shouldReloadOption === 'function' - ? shouldReloadOption(getLoaderContext(inner, matchId, index, route)) - : shouldReloadOption - - const nextPreload = - !!preload && !inner.router.state.matches.some((d) => d.id === matchId) - const match = inner.router.getMatch(matchId)! - match._nonReactive.loaderPromise = createControlledPromise() - if (nextPreload !== match.preload) { - inner.updateMatch(matchId, (prev) => ({ - ...prev, - preload: nextPreload, - })) - } + return settleLoadRouteMatch() + }) + } + + // This is where all of the stale-while-revalidate magic happens + const age = Date.now() - prevMatch.updatedAt + + const preload = resolvePreload(inner, matchId) + + const staleAge = preload + ? (route.options.preloadStaleTime ?? + inner.router.options.defaultPreloadStaleTime ?? + 30_000) // 30 seconds for preloads by default + : (route.options.staleTime ?? inner.router.options.defaultStaleTime ?? 0) + + const shouldReloadOption = route.options.shouldReload + + // Default to reloading the route all the time + // Allow shouldReload to get the last say, + // if provided. + const shouldReload = + typeof shouldReloadOption === 'function' + ? shouldReloadOption(getLoaderContext(inner, matchId, index, route)) + : shouldReloadOption + + const nextPreload = + !!preload && !inner.router.state.matches.some((d) => d.id === matchId) + const match = inner.router.getMatch(matchId)! + match._nonReactive.loaderPromise = createControlledPromise() + if (nextPreload !== match.preload) { + inner.updateMatch(matchId, (prev) => ({ + ...prev, + preload: nextPreload, + })) + } - // If the route is successful and still fresh, just resolve - const { status, invalid } = match - loaderShouldRunAsync = - status === 'success' && (invalid || (shouldReload ?? age > staleAge)) - if (preload && route.options.preload === false) { - // Do nothing - } else if (loaderShouldRunAsync && !inner.sync) { - loaderIsRunningAsync = true - ;(async () => { - try { - await runLoader(inner, matchId, index, route) - const match = inner.router.getMatch(matchId)! - match._nonReactive.loaderPromise?.resolve() - match._nonReactive.loadPromise?.resolve() - match._nonReactive.loaderPromise = undefined - } catch (err) { - if (isRedirect(err)) { - await inner.router.navigate(err.options) - } - } - })() - } else if (status !== 'success' || (loaderShouldRunAsync && inner.sync)) { + if (preload && route.options.preload === false) { + // Do nothing + return settleLoadRouteMatch() + } + + // If the route is successful and still fresh, just resolve + const { status, invalid } = match + loaderShouldRunAsync = + status === 'success' && (invalid || (shouldReload ?? age > staleAge)) + if (loaderShouldRunAsync && !inner.sync) { + loaderIsRunningAsync = true + ;(async () => { + try { await runLoader(inner, matchId, index, route) - } else { - // if the loader did not run, still update head. - // reason: parent's beforeLoad may have changed the route context - // and only now do we know the route context (and that the loader would not run) - const headResult = executeHead(inner, matchId, route) - if (headResult) { - const head = await headResult - inner.updateMatch(matchId, (prev) => ({ - ...prev, - ...head, - })) + const match = inner.router.getMatch(matchId)! + match._nonReactive.loaderPromise?.resolve() + match._nonReactive.loadPromise?.resolve() + match._nonReactive.loaderPromise = undefined + } catch (err) { + if (isRedirect(err)) { + await inner.router.navigate(err.options) } } - } + })() + return settleLoadRouteMatch() } - const match = inner.router.getMatch(matchId)! - if (!loaderIsRunningAsync) { - match._nonReactive.loaderPromise?.resolve() - match._nonReactive.loadPromise?.resolve() + + if (status !== 'success' || (loaderShouldRunAsync && inner.sync)) { + return runLoader(inner, matchId, index, route).then(settleLoadRouteMatch) } - clearTimeout(match._nonReactive.pendingTimeout) - match._nonReactive.pendingTimeout = undefined - if (!loaderIsRunningAsync) match._nonReactive.loaderPromise = undefined - match._nonReactive.dehydrated = undefined - const nextIsFetching = loaderIsRunningAsync ? match.isFetching : false - if (nextIsFetching !== match.isFetching || match.invalid !== false) { - inner.updateMatch(matchId, (prev) => ({ - ...prev, - isFetching: nextIsFetching, - invalid: false, - })) - return inner.router.getMatch(matchId)! - } else { + // if the loader did not run, still update head. + // reason: parent's beforeLoad may have changed the route context + // and only now do we know the route context (and that the loader would not run) + const headResult = executeHead(inner, matchId, route) + if (headResult) { + return headResult.then((head) => { + inner.updateMatch(matchId, (prev) => ({ + ...prev, + ...head, + })) + return settleLoadRouteMatch() + }) + } + + return settleLoadRouteMatch() + + function settleLoadRouteMatch() { + const match = inner.router.getMatch(matchId)! + + if (!loaderIsRunningAsync) { + match._nonReactive.loaderPromise?.resolve() + match._nonReactive.loadPromise?.resolve() + match._nonReactive.loaderPromise = undefined + } + + clearTimeout(match._nonReactive.pendingTimeout) + match._nonReactive.pendingTimeout = undefined + match._nonReactive.dehydrated = undefined + + const nextIsFetching = loaderIsRunningAsync ? match.isFetching : false + if (nextIsFetching !== match.isFetching || match.invalid !== false) { + inner.updateMatch(matchId, (prev) => ({ + ...prev, + isFetching: nextIsFetching, + invalid: false, + })) + return inner.router.getMatch(matchId)! + } + return match } } @@ -866,10 +886,13 @@ export async function loadMatches(arg: { // Execute all loaders in parallel const max = inner.firstBadMatchIndex ?? inner.matches.length + let hasPromises = false for (let i = 0; i < max; i++) { - inner.matchPromises.push(loadRouteMatch(inner, i)) + const result = loadRouteMatch(inner, i) + inner.matchPromises.push(result) + if (!hasPromises && isPromise(result)) hasPromises = true } - await Promise.all(inner.matchPromises) + if (hasPromises) await Promise.all(inner.matchPromises) const readyPromise = triggerOnReady(inner) if (isPromise(readyPromise)) await readyPromise diff --git a/packages/router-core/src/route.ts b/packages/router-core/src/route.ts index bdda040845..56fdbd46c6 100644 --- a/packages/router-core/src/route.ts +++ b/packages/router-core/src/route.ts @@ -1236,7 +1236,7 @@ export interface LoaderFnContext< // root route does not have a parent match parentMatchPromise: TId extends RootRouteId ? never - : Promise> + : Awaitable> cause: 'preload' | 'enter' | 'stay' route: AnyRoute } From 1a6d2623e60026ac27bfaec63fdce865aa4a868d Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 23 Aug 2025 13:37:31 +0200 Subject: [PATCH 2/8] type-safe parentMatchPromise --- .../framework/react/api/router/RouteOptionsType.md | 5 ++++- packages/router-core/src/route.ts | 10 +++++++++- packages/router-core/src/utils.ts | 8 ++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/router/framework/react/api/router/RouteOptionsType.md b/docs/router/framework/react/api/router/RouteOptionsType.md index a489fe2423..464cc07e60 100644 --- a/docs/router/framework/react/api/router/RouteOptionsType.md +++ b/docs/router/framework/react/api/router/RouteOptionsType.md @@ -131,7 +131,9 @@ type loader = ( location: ParsedLocation params: TAllParams preload: boolean - parentMatchPromise: Promise> + parentMatchPromise: + | Promise> + | MakeRouteMatchFromRoute navigate: NavigateFn // @deprecated route: AnyRoute }, @@ -144,6 +146,7 @@ type loader = ( - If this function returns a promise, the route will be put into a pending state and cause rendering to suspend until the promise resolves. If this route's pendingMs threshold is reached, the `pendingComponent` will be shown until it resolves. If the promise rejects, the route will be put into an error state and the error will be thrown during render. - If this function returns a `TLoaderData` object, that object will be stored on the route match until the route match is no longer active. It can be accessed using the `useLoaderData` hook in any component that is a child of the route match before another `` is rendered. - Deps must be returned by your `loaderDeps` function in order to appear. +- `parentMatchPromise` is a promise *if* the parent route's `loader` returns a promise as well, otherwise it is the resolved parent match object. > 🚧 `opts.navigate` has been deprecated and will be removed in the next major release. Use `throw redirect({ to: '/somewhere' })` instead. Read more about the `redirect` function [here](../redirectFunction.md). diff --git a/packages/router-core/src/route.ts b/packages/router-core/src/route.ts index 56fdbd46c6..b22903da83 100644 --- a/packages/router-core/src/route.ts +++ b/packages/router-core/src/route.ts @@ -24,6 +24,7 @@ import type { Expand, IntersectAssign, LooseAsyncReturnType, + LooseIsReturnPromise, LooseReturnType, NoInfer, } from './utils' @@ -421,6 +422,11 @@ export interface RouteTypes< children: TChildren loaderData: ResolveLoaderData loaderDeps: TLoaderDeps + asyncLoaderFn: unknown extends TLoaderFn + ? boolean + : LooseIsReturnPromise extends never + ? boolean + : LooseIsReturnPromise fileRouteTypes: TFileRouteTypes } @@ -1236,7 +1242,9 @@ export interface LoaderFnContext< // root route does not have a parent match parentMatchPromise: TId extends RootRouteId ? never - : Awaitable> + : TParentRoute['types']['asyncLoaderFn'] extends true + ? Promise> + : MakeRouteMatchFromRoute cause: 'preload' | 'enter' | 'stay' route: AnyRoute } diff --git a/packages/router-core/src/utils.ts b/packages/router-core/src/utils.ts index d446509498..8ae6a55fb5 100644 --- a/packages/router-core/src/utils.ts +++ b/packages/router-core/src/utils.ts @@ -184,6 +184,14 @@ export type LooseAsyncReturnType = T extends ( : TReturn : never +export type LooseIsReturnPromise = T extends ( + ...args: Array +) => infer TReturn + ? TReturn extends Promise + ? true + : false + : never + export function last(arr: Array) { return arr[arr.length - 1] } From 0c1891331448cd0dd22aa279b60b1cdacf77eeb2 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 23 Aug 2025 11:38:24 +0000 Subject: [PATCH 3/8] ci: apply automated fixes --- docs/router/framework/react/api/router/RouteOptionsType.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/router/framework/react/api/router/RouteOptionsType.md b/docs/router/framework/react/api/router/RouteOptionsType.md index 464cc07e60..4a25deff59 100644 --- a/docs/router/framework/react/api/router/RouteOptionsType.md +++ b/docs/router/framework/react/api/router/RouteOptionsType.md @@ -146,7 +146,7 @@ type loader = ( - If this function returns a promise, the route will be put into a pending state and cause rendering to suspend until the promise resolves. If this route's pendingMs threshold is reached, the `pendingComponent` will be shown until it resolves. If the promise rejects, the route will be put into an error state and the error will be thrown during render. - If this function returns a `TLoaderData` object, that object will be stored on the route match until the route match is no longer active. It can be accessed using the `useLoaderData` hook in any component that is a child of the route match before another `` is rendered. - Deps must be returned by your `loaderDeps` function in order to appear. -- `parentMatchPromise` is a promise *if* the parent route's `loader` returns a promise as well, otherwise it is the resolved parent match object. +- `parentMatchPromise` is a promise _if_ the parent route's `loader` returns a promise as well, otherwise it is the resolved parent match object. > 🚧 `opts.navigate` has been deprecated and will be removed in the next major release. Use `throw redirect({ to: '/somewhere' })` instead. Read more about the `redirect` function [here](../redirectFunction.md). From b070a6821462fafe03de4aeb54fb0fc57ae71bf5 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 23 Aug 2025 14:45:31 +0200 Subject: [PATCH 4/8] fewer store updates in sync loadRouteMatch --- packages/router-core/src/load-matches.ts | 76 ++++++++++++++---------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/packages/router-core/src/load-matches.ts b/packages/router-core/src/load-matches.ts index 72c1709618..cb3b91ec70 100644 --- a/packages/router-core/src/load-matches.ts +++ b/packages/router-core/src/load-matches.ts @@ -709,9 +709,10 @@ const loadRouteMatch = ( index: number, ): Awaitable => { const { id: matchId, routeId } = inner.matches[index]! - let loaderShouldRunAsync = false - let loaderIsRunningAsync = false const route = inner.router.looseRoutesById[routeId]! + const prevMatch = inner.router.getMatch(matchId)! + let loaderIsRunningAsync = false + let nextPreload: undefined | boolean if (shouldSkipLoader(inner, matchId)) { if (inner.router.isServer) { @@ -725,13 +726,11 @@ const loadRouteMatch = ( return inner.router.getMatch(matchId)! }) } - return inner.router.getMatch(matchId)! + return prevMatch } return settleLoadRouteMatch() } - const prevMatch = inner.router.getMatch(matchId)! - // there is a loaderPromise, so we are in the middle of a load if (prevMatch._nonReactive.loaderPromise) { // do not block if we already have stale data we can show @@ -770,16 +769,9 @@ const loadRouteMatch = ( ? shouldReloadOption(getLoaderContext(inner, matchId, index, route)) : shouldReloadOption - const nextPreload = + nextPreload = !!preload && !inner.router.state.matches.some((d) => d.id === matchId) - const match = inner.router.getMatch(matchId)! - match._nonReactive.loaderPromise = createControlledPromise() - if (nextPreload !== match.preload) { - inner.updateMatch(matchId, (prev) => ({ - ...prev, - preload: nextPreload, - })) - } + prevMatch._nonReactive.loaderPromise = createControlledPromise() if (preload && route.options.preload === false) { // Do nothing @@ -787,8 +779,8 @@ const loadRouteMatch = ( } // If the route is successful and still fresh, just resolve - const { status, invalid } = match - loaderShouldRunAsync = + const { status, invalid } = prevMatch + const loaderShouldRunAsync = status === 'success' && (invalid || (shouldReload ?? age > staleAge)) if (loaderShouldRunAsync && !inner.sync) { loaderIsRunningAsync = true @@ -809,6 +801,7 @@ const loadRouteMatch = ( } if (status !== 'success' || (loaderShouldRunAsync && inner.sync)) { + updatePreload() return runLoader(inner, matchId, index, route).then(settleLoadRouteMatch) } @@ -818,16 +811,32 @@ const loadRouteMatch = ( const headResult = executeHead(inner, matchId, route) if (headResult) { return headResult.then((head) => { - inner.updateMatch(matchId, (prev) => ({ - ...prev, - ...head, - })) - return settleLoadRouteMatch() + let result: ReturnType + batch(() => { + inner.updateMatch(matchId, (prev) => ({ + ...prev, + ...head, + })) + result = settleLoadRouteMatch() + }) + return result! }) } return settleLoadRouteMatch() + function updatePreload() { + if (nextPreload === undefined) return + if (nextPreload !== prevMatch.preload) { + const preload = nextPreload + inner.updateMatch(matchId, (prev) => ({ + ...prev, + preload, + })) + } + nextPreload = undefined + } + function settleLoadRouteMatch() { const match = inner.router.getMatch(matchId)! @@ -843,15 +852,19 @@ const loadRouteMatch = ( const nextIsFetching = loaderIsRunningAsync ? match.isFetching : false if (nextIsFetching !== match.isFetching || match.invalid !== false) { - inner.updateMatch(matchId, (prev) => ({ - ...prev, - isFetching: nextIsFetching, - invalid: false, - })) - return inner.router.getMatch(matchId)! + batch(() => { + updatePreload() + inner.updateMatch(matchId, (prev) => ({ + ...prev, + isFetching: nextIsFetching, + invalid: false, + })) + }) + } else { + updatePreload() } - return match + return inner.router.getMatch(matchId)! } } @@ -886,13 +899,10 @@ export async function loadMatches(arg: { // Execute all loaders in parallel const max = inner.firstBadMatchIndex ?? inner.matches.length - let hasPromises = false for (let i = 0; i < max; i++) { - const result = loadRouteMatch(inner, i) - inner.matchPromises.push(result) - if (!hasPromises && isPromise(result)) hasPromises = true + inner.matchPromises.push(loadRouteMatch(inner, i)) } - if (hasPromises) await Promise.all(inner.matchPromises) + await Promise.all(inner.matchPromises) const readyPromise = triggerOnReady(inner) if (isPromise(readyPromise)) await readyPromise From 9ea4bc772b3f2ba39b282cb9cc97d8fa1bdbcf27 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 23 Aug 2025 16:04:07 +0200 Subject: [PATCH 5/8] revert batching as i cannot measure the reduction --- packages/router-core/src/load-matches.ts | 30 +++++++----------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/packages/router-core/src/load-matches.ts b/packages/router-core/src/load-matches.ts index cb3b91ec70..799b1345c4 100644 --- a/packages/router-core/src/load-matches.ts +++ b/packages/router-core/src/load-matches.ts @@ -712,7 +712,6 @@ const loadRouteMatch = ( const route = inner.router.looseRoutesById[routeId]! const prevMatch = inner.router.getMatch(matchId)! let loaderIsRunningAsync = false - let nextPreload: undefined | boolean if (shouldSkipLoader(inner, matchId)) { if (inner.router.isServer) { @@ -769,9 +768,15 @@ const loadRouteMatch = ( ? shouldReloadOption(getLoaderContext(inner, matchId, index, route)) : shouldReloadOption - nextPreload = + const nextPreload = !!preload && !inner.router.state.matches.some((d) => d.id === matchId) prevMatch._nonReactive.loaderPromise = createControlledPromise() + if (nextPreload !== prevMatch.preload) { + inner.updateMatch(matchId, (prev) => ({ + ...prev, + preload: nextPreload, + })) + } if (preload && route.options.preload === false) { // Do nothing @@ -801,7 +806,6 @@ const loadRouteMatch = ( } if (status !== 'success' || (loaderShouldRunAsync && inner.sync)) { - updatePreload() return runLoader(inner, matchId, index, route).then(settleLoadRouteMatch) } @@ -825,18 +829,6 @@ const loadRouteMatch = ( return settleLoadRouteMatch() - function updatePreload() { - if (nextPreload === undefined) return - if (nextPreload !== prevMatch.preload) { - const preload = nextPreload - inner.updateMatch(matchId, (prev) => ({ - ...prev, - preload, - })) - } - nextPreload = undefined - } - function settleLoadRouteMatch() { const match = inner.router.getMatch(matchId)! @@ -852,19 +844,15 @@ const loadRouteMatch = ( const nextIsFetching = loaderIsRunningAsync ? match.isFetching : false if (nextIsFetching !== match.isFetching || match.invalid !== false) { - batch(() => { - updatePreload() inner.updateMatch(matchId, (prev) => ({ ...prev, isFetching: nextIsFetching, invalid: false, })) - }) - } else { - updatePreload() + return inner.router.getMatch(matchId)! } - return inner.router.getMatch(matchId)! + return match } } From 23732002af0c239193cc1baa56224f1a2db751ad Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 23 Aug 2025 14:05:01 +0000 Subject: [PATCH 6/8] ci: apply automated fixes --- packages/router-core/src/load-matches.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/router-core/src/load-matches.ts b/packages/router-core/src/load-matches.ts index 799b1345c4..841fac9f21 100644 --- a/packages/router-core/src/load-matches.ts +++ b/packages/router-core/src/load-matches.ts @@ -844,11 +844,11 @@ const loadRouteMatch = ( const nextIsFetching = loaderIsRunningAsync ? match.isFetching : false if (nextIsFetching !== match.isFetching || match.invalid !== false) { - inner.updateMatch(matchId, (prev) => ({ - ...prev, - isFetching: nextIsFetching, - invalid: false, - })) + inner.updateMatch(matchId, (prev) => ({ + ...prev, + isFetching: nextIsFetching, + invalid: false, + })) return inner.router.getMatch(matchId)! } From 8b9e00ada945242fc6536cdd08c77183f45bc215 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 23 Aug 2025 16:09:54 +0200 Subject: [PATCH 7/8] revert public API change --- .../react/api/router/RouteOptionsType.md | 5 +- packages/router-core/src/load-matches.ts | 2 +- packages/router-core/src/route.ts | 10 +- packages/router-core/src/utils.ts | 122 ++++++++---------- 4 files changed, 60 insertions(+), 79 deletions(-) diff --git a/docs/router/framework/react/api/router/RouteOptionsType.md b/docs/router/framework/react/api/router/RouteOptionsType.md index 4a25deff59..a489fe2423 100644 --- a/docs/router/framework/react/api/router/RouteOptionsType.md +++ b/docs/router/framework/react/api/router/RouteOptionsType.md @@ -131,9 +131,7 @@ type loader = ( location: ParsedLocation params: TAllParams preload: boolean - parentMatchPromise: - | Promise> - | MakeRouteMatchFromRoute + parentMatchPromise: Promise> navigate: NavigateFn // @deprecated route: AnyRoute }, @@ -146,7 +144,6 @@ type loader = ( - If this function returns a promise, the route will be put into a pending state and cause rendering to suspend until the promise resolves. If this route's pendingMs threshold is reached, the `pendingComponent` will be shown until it resolves. If the promise rejects, the route will be put into an error state and the error will be thrown during render. - If this function returns a `TLoaderData` object, that object will be stored on the route match until the route match is no longer active. It can be accessed using the `useLoaderData` hook in any component that is a child of the route match before another `` is rendered. - Deps must be returned by your `loaderDeps` function in order to appear. -- `parentMatchPromise` is a promise _if_ the parent route's `loader` returns a promise as well, otherwise it is the resolved parent match object. > 🚧 `opts.navigate` has been deprecated and will be removed in the next major release. Use `throw redirect({ to: '/somewhere' })` instead. Read more about the `redirect` function [here](../redirectFunction.md). diff --git a/packages/router-core/src/load-matches.ts b/packages/router-core/src/load-matches.ts index 841fac9f21..12a2576700 100644 --- a/packages/router-core/src/load-matches.ts +++ b/packages/router-core/src/load-matches.ts @@ -551,7 +551,7 @@ const getLoaderContext = ( index: number, route: AnyRoute, ): LoaderFnContext => { - const parentMatchPromise = inner.matchPromises[index - 1] as any + const parentMatchPromise = Promise.resolve(inner.matchPromises[index - 1] as any) const { params, loaderDeps, abortController, context, cause } = inner.router.getMatch(matchId)! diff --git a/packages/router-core/src/route.ts b/packages/router-core/src/route.ts index b22903da83..bdda040845 100644 --- a/packages/router-core/src/route.ts +++ b/packages/router-core/src/route.ts @@ -24,7 +24,6 @@ import type { Expand, IntersectAssign, LooseAsyncReturnType, - LooseIsReturnPromise, LooseReturnType, NoInfer, } from './utils' @@ -422,11 +421,6 @@ export interface RouteTypes< children: TChildren loaderData: ResolveLoaderData loaderDeps: TLoaderDeps - asyncLoaderFn: unknown extends TLoaderFn - ? boolean - : LooseIsReturnPromise extends never - ? boolean - : LooseIsReturnPromise fileRouteTypes: TFileRouteTypes } @@ -1242,9 +1236,7 @@ export interface LoaderFnContext< // root route does not have a parent match parentMatchPromise: TId extends RootRouteId ? never - : TParentRoute['types']['asyncLoaderFn'] extends true - ? Promise> - : MakeRouteMatchFromRoute + : Promise> cause: 'preload' | 'enter' | 'stay' route: AnyRoute } diff --git a/packages/router-core/src/utils.ts b/packages/router-core/src/utils.ts index 8ae6a55fb5..9cb68dd153 100644 --- a/packages/router-core/src/utils.ts +++ b/packages/router-core/src/utils.ts @@ -26,24 +26,24 @@ export type WithoutEmpty = T extends any ? ({} extends T ? never : T) : never export type Expand = T extends object ? T extends infer O - ? O extends Function - ? O - : { [K in keyof O]: O[K] } - : never + ? O extends Function + ? O + : { [K in keyof O]: O[K] } + : never : T export type DeepPartial = T extends object ? { - [P in keyof T]?: DeepPartial - } + [P in keyof T]?: DeepPartial + } : T export type MakeDifferenceOptional = keyof TLeft & keyof TRight extends never ? TRight : Omit & { - [K in keyof TLeft & keyof TRight]?: TRight[K] - } + [K in keyof TLeft & keyof TRight]?: TRight[K] + } // from https://stackoverflow.com/a/53955431 // eslint-disable-next-line @typescript-eslint/naming-convention @@ -55,30 +55,30 @@ export type IsUnion = ( export type IsNonEmptyObject = T extends object ? keyof T extends never - ? false - : true + ? false + : true : false export type Assign = TLeft extends any ? TRight extends any - ? IsNonEmptyObject extends false - ? TRight - : IsNonEmptyObject extends false - ? TLeft - : keyof TLeft & keyof TRight extends never - ? TLeft & TRight - : Omit & TRight - : never + ? IsNonEmptyObject extends false + ? TRight + : IsNonEmptyObject extends false + ? TLeft + : keyof TLeft & keyof TRight extends never + ? TLeft & TRight + : Omit & TRight + : never : never export type IntersectAssign = TLeft extends any ? TRight extends any - ? IsNonEmptyObject extends false - ? TRight - : IsNonEmptyObject extends false - ? TLeft - : TRight & TLeft - : never + ? IsNonEmptyObject extends false + ? TRight + : IsNonEmptyObject extends false + ? TLeft + : TRight & TLeft + : never : never export type Timeout = ReturnType @@ -97,16 +97,16 @@ export type ExtractObjects = TUnion extends MergeAllPrimitive export type PartialMergeAllObject = ExtractObjects extends infer TObj - ? [TObj] extends [never] - ? never - : { - [TKey in TObj extends any ? keyof TObj : never]?: TObj extends any - ? TKey extends keyof TObj - ? TObj[TKey] - : never - : never - } + ? [TObj] extends [never] + ? never + : { + [TKey in TObj extends any ? keyof TObj : never]?: TObj extends any + ? TKey extends keyof TObj + ? TObj[TKey] + : never : never + } + : never export type MergeAllPrimitive = | ReadonlyArray @@ -121,8 +121,8 @@ export type MergeAllPrimitive = export type ExtractPrimitives = TUnion extends MergeAllPrimitive ? TUnion : TUnion extends object - ? never - : TUnion + ? never + : TUnion export type PartialMergeAll = | ExtractPrimitives @@ -155,10 +155,10 @@ export type MergeAllObjects< > = [keyof TIntersected] extends [never] ? never : { - [TKey in keyof TIntersected]: TUnion extends any - ? TUnion[TKey & keyof TUnion] - : never - } + [TKey in keyof TIntersected]: TUnion extends any + ? TUnion[TKey & keyof TUnion] + : never + } export type MergeAll = | MergeAllObjects @@ -166,8 +166,8 @@ export type MergeAll = export type ValidateJSON = ((...args: Array) => any) extends T ? unknown extends T - ? never - : 'Function is not serializable' + ? never + : 'Function is not serializable' : { [K in keyof T]: ValidateJSON } export type LooseReturnType = T extends ( @@ -180,16 +180,8 @@ export type LooseAsyncReturnType = T extends ( ...args: Array ) => infer TReturn ? TReturn extends Promise - ? TReturn - : TReturn - : never - -export type LooseIsReturnPromise = T extends ( - ...args: Array -) => infer TReturn - ? TReturn extends Promise - ? true - : false + ? TReturn + : TReturn : never export function last(arr: Array) { @@ -230,14 +222,14 @@ export function replaceEqualDeep(prev: any, _next: T): T { const prevItems = array ? prev : (Object.keys(prev) as Array).concat( - Object.getOwnPropertySymbols(prev), - ) + Object.getOwnPropertySymbols(prev), + ) const prevSize = prevItems.length const nextItems = array ? next : (Object.keys(next) as Array).concat( - Object.getOwnPropertySymbols(next), - ) + Object.getOwnPropertySymbols(next), + ) const nextSize = nextItems.length const copy: any = array ? [] : {} @@ -359,8 +351,8 @@ export function deepEqual( export type StringLiteral = T extends string ? string extends T - ? string - : T + ? string + : T : never export type ThrowOrOptional = TThrow extends true @@ -373,13 +365,13 @@ export type StrictOrFrom< TStrict extends boolean = true, > = TStrict extends false ? { - from?: never - strict: TStrict - } + from?: never + strict: TStrict + } : { - from: ConstrainLiteral> - strict?: TStrict - } + from: ConstrainLiteral> + strict?: TStrict + } export type ThrowConstraint< TStrict extends boolean, @@ -477,8 +469,8 @@ export function isPromise( ): value is Promise> { return Boolean( value && - typeof value === 'object' && - typeof (value as Promise).then === 'function', + typeof value === 'object' && + typeof (value as Promise).then === 'function', ) } From fead952b888992f1cc3410edb4862cebcbd42652 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 23 Aug 2025 14:10:45 +0000 Subject: [PATCH 8/8] ci: apply automated fixes --- packages/router-core/src/load-matches.ts | 4 +- packages/router-core/src/utils.ts | 114 +++++++++++------------ 2 files changed, 60 insertions(+), 58 deletions(-) diff --git a/packages/router-core/src/load-matches.ts b/packages/router-core/src/load-matches.ts index 12a2576700..b8a6d0fc3b 100644 --- a/packages/router-core/src/load-matches.ts +++ b/packages/router-core/src/load-matches.ts @@ -551,7 +551,9 @@ const getLoaderContext = ( index: number, route: AnyRoute, ): LoaderFnContext => { - const parentMatchPromise = Promise.resolve(inner.matchPromises[index - 1] as any) + const parentMatchPromise = Promise.resolve( + inner.matchPromises[index - 1] as any, + ) const { params, loaderDeps, abortController, context, cause } = inner.router.getMatch(matchId)! diff --git a/packages/router-core/src/utils.ts b/packages/router-core/src/utils.ts index 9cb68dd153..d446509498 100644 --- a/packages/router-core/src/utils.ts +++ b/packages/router-core/src/utils.ts @@ -26,24 +26,24 @@ export type WithoutEmpty = T extends any ? ({} extends T ? never : T) : never export type Expand = T extends object ? T extends infer O - ? O extends Function - ? O - : { [K in keyof O]: O[K] } - : never + ? O extends Function + ? O + : { [K in keyof O]: O[K] } + : never : T export type DeepPartial = T extends object ? { - [P in keyof T]?: DeepPartial - } + [P in keyof T]?: DeepPartial + } : T export type MakeDifferenceOptional = keyof TLeft & keyof TRight extends never ? TRight : Omit & { - [K in keyof TLeft & keyof TRight]?: TRight[K] - } + [K in keyof TLeft & keyof TRight]?: TRight[K] + } // from https://stackoverflow.com/a/53955431 // eslint-disable-next-line @typescript-eslint/naming-convention @@ -55,30 +55,30 @@ export type IsUnion = ( export type IsNonEmptyObject = T extends object ? keyof T extends never - ? false - : true + ? false + : true : false export type Assign = TLeft extends any ? TRight extends any - ? IsNonEmptyObject extends false - ? TRight - : IsNonEmptyObject extends false - ? TLeft - : keyof TLeft & keyof TRight extends never - ? TLeft & TRight - : Omit & TRight - : never + ? IsNonEmptyObject extends false + ? TRight + : IsNonEmptyObject extends false + ? TLeft + : keyof TLeft & keyof TRight extends never + ? TLeft & TRight + : Omit & TRight + : never : never export type IntersectAssign = TLeft extends any ? TRight extends any - ? IsNonEmptyObject extends false - ? TRight - : IsNonEmptyObject extends false - ? TLeft - : TRight & TLeft - : never + ? IsNonEmptyObject extends false + ? TRight + : IsNonEmptyObject extends false + ? TLeft + : TRight & TLeft + : never : never export type Timeout = ReturnType @@ -97,16 +97,16 @@ export type ExtractObjects = TUnion extends MergeAllPrimitive export type PartialMergeAllObject = ExtractObjects extends infer TObj - ? [TObj] extends [never] - ? never - : { - [TKey in TObj extends any ? keyof TObj : never]?: TObj extends any - ? TKey extends keyof TObj - ? TObj[TKey] - : never + ? [TObj] extends [never] + ? never + : { + [TKey in TObj extends any ? keyof TObj : never]?: TObj extends any + ? TKey extends keyof TObj + ? TObj[TKey] + : never + : never + } : never - } - : never export type MergeAllPrimitive = | ReadonlyArray @@ -121,8 +121,8 @@ export type MergeAllPrimitive = export type ExtractPrimitives = TUnion extends MergeAllPrimitive ? TUnion : TUnion extends object - ? never - : TUnion + ? never + : TUnion export type PartialMergeAll = | ExtractPrimitives @@ -155,10 +155,10 @@ export type MergeAllObjects< > = [keyof TIntersected] extends [never] ? never : { - [TKey in keyof TIntersected]: TUnion extends any - ? TUnion[TKey & keyof TUnion] - : never - } + [TKey in keyof TIntersected]: TUnion extends any + ? TUnion[TKey & keyof TUnion] + : never + } export type MergeAll = | MergeAllObjects @@ -166,8 +166,8 @@ export type MergeAll = export type ValidateJSON = ((...args: Array) => any) extends T ? unknown extends T - ? never - : 'Function is not serializable' + ? never + : 'Function is not serializable' : { [K in keyof T]: ValidateJSON } export type LooseReturnType = T extends ( @@ -180,8 +180,8 @@ export type LooseAsyncReturnType = T extends ( ...args: Array ) => infer TReturn ? TReturn extends Promise - ? TReturn - : TReturn + ? TReturn + : TReturn : never export function last(arr: Array) { @@ -222,14 +222,14 @@ export function replaceEqualDeep(prev: any, _next: T): T { const prevItems = array ? prev : (Object.keys(prev) as Array).concat( - Object.getOwnPropertySymbols(prev), - ) + Object.getOwnPropertySymbols(prev), + ) const prevSize = prevItems.length const nextItems = array ? next : (Object.keys(next) as Array).concat( - Object.getOwnPropertySymbols(next), - ) + Object.getOwnPropertySymbols(next), + ) const nextSize = nextItems.length const copy: any = array ? [] : {} @@ -351,8 +351,8 @@ export function deepEqual( export type StringLiteral = T extends string ? string extends T - ? string - : T + ? string + : T : never export type ThrowOrOptional = TThrow extends true @@ -365,13 +365,13 @@ export type StrictOrFrom< TStrict extends boolean = true, > = TStrict extends false ? { - from?: never - strict: TStrict - } + from?: never + strict: TStrict + } : { - from: ConstrainLiteral> - strict?: TStrict - } + from: ConstrainLiteral> + strict?: TStrict + } export type ThrowConstraint< TStrict extends boolean, @@ -469,8 +469,8 @@ export function isPromise( ): value is Promise> { return Boolean( value && - typeof value === 'object' && - typeof (value as Promise).then === 'function', + typeof value === 'object' && + typeof (value as Promise).then === 'function', ) }