diff --git a/packages/react-router/src/Match.tsx b/packages/react-router/src/Match.tsx index 67421e40d9..117299d4b0 100644 --- a/packages/react-router/src/Match.tsx +++ b/packages/react-router/src/Match.tsx @@ -248,9 +248,7 @@ export const MatchInner = React.memo(function MatchInnerImpl({ if (!router.isServer) { const minPendingPromise = createControlledPromise() - Promise.resolve().then(() => { - routerMatch._nonReactive.minPendingPromise = minPendingPromise - }) + routerMatch._nonReactive.minPendingPromise = minPendingPromise setTimeout(() => { minPendingPromise.resolve() diff --git a/packages/react-router/tests/store-updates-during-navigation.test.tsx b/packages/react-router/tests/store-updates-during-navigation.test.tsx index 33e3443fdc..19507e182e 100644 --- a/packages/react-router/tests/store-updates-during-navigation.test.tsx +++ b/packages/react-router/tests/store-updates-during-navigation.test.tsx @@ -110,7 +110,7 @@ describe("Store doesn't update *too many* times during navigation", () => { // This number should be as small as possible to minimize the amount of work // that needs to be done during a navigation. // Any change that increases this number should be investigated. - expect(updates).toBe(19) + expect(updates).toBe(17) }) test('redirection in preload', async () => { diff --git a/packages/router-core/src/route.ts b/packages/router-core/src/route.ts index b89549cdc6..bdda040845 100644 --- a/packages/router-core/src/route.ts +++ b/packages/router-core/src/route.ts @@ -589,7 +589,10 @@ export interface Route< TBeforeLoadFn > isRoot: TParentRoute extends AnyRoute ? true : false - _componentsPromise?: Promise> + /** @internal */ + _componentsPromise?: Promise + /** @internal */ + _componentsLoaded?: boolean lazyFn?: () => Promise< LazyRoute< Route< @@ -610,7 +613,10 @@ export interface Route< > > > + /** @internal */ _lazyPromise?: Promise + /** @internal */ + _lazyLoaded?: boolean rank: number to: TrimPathRight init: (opts: { originalIndex: number }) => void @@ -1406,8 +1412,10 @@ export class BaseRoute< > > > + /** @internal */ _lazyPromise?: Promise - _componentsPromise?: Promise> + /** @internal */ + _componentsPromise?: Promise constructor( options?: RouteOptions< diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index d46cf9119b..adc0e1c9f7 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -9,6 +9,7 @@ import { createControlledPromise, deepEqual, functionalUpdate, + isPromise, last, pick, replaceEqualDeep, @@ -2508,11 +2509,9 @@ export class RouterCore< }) } - const potentialPendingMinPromise = async () => { + const potentialPendingMinPromise = () => { const latestMatch = this.getMatch(matchId)! - if (latestMatch._nonReactive.minPendingPromise) { - await latestMatch._nonReactive.minPendingPromise - } + return latestMatch._nonReactive.minPendingPromise } const prevMatch = this.getMatch(matchId)! @@ -2621,41 +2620,63 @@ export class RouterCore< try { if ( !this.isServer || - (this.isServer && - this.getMatch(matchId)!.ssr === true) + this.getMatch(matchId)!.ssr === true ) { this.loadRouteChunk(route) } - updateMatch(matchId, (prev) => ({ - ...prev, - isFetching: 'loader', - })) - // Kick off the loader! - const loaderData = - await route.options.loader?.(getLoaderContext()) - - handleRedirectAndNotFound( - this.getMatch(matchId), - loaderData, + const loaderResult = + route.options.loader?.(getLoaderContext()) + const loaderResultIsPromise = + route.options.loader && isPromise(loaderResult) + + const willLoadSomething = !!( + loaderResultIsPromise || + route._lazyPromise || + route._componentsPromise || + route.options.head || + route.options.scripts || + route.options.headers || + this.getMatch(matchId)!._nonReactive + .minPendingPromise ) - updateMatch(matchId, (prev) => ({ - ...prev, - loaderData, - })) + + if (willLoadSomething) { + updateMatch(matchId, (prev) => ({ + ...prev, + isFetching: 'loader', + })) + } + + if (route.options.loader) { + const loaderData = loaderResultIsPromise + ? await loaderResult + : loaderResult + + handleRedirectAndNotFound( + this.getMatch(matchId), + loaderData, + ) + updateMatch(matchId, (prev) => ({ + ...prev, + loaderData, + })) + } // Lazy option can modify the route options, // so we need to wait for it to resolve before // we can use the options - await route._lazyPromise + if (route._lazyPromise) await route._lazyPromise const headResult = executeHead() const head = headResult ? await headResult : undefined - await potentialPendingMinPromise() + const pendingPromise = potentialPendingMinPromise() + if (pendingPromise) await pendingPromise // Last but not least, wait for the the components // to be preloaded before we resolve the match - await route._componentsPromise + if (route._componentsPromise) + await route._componentsPromise updateMatch(matchId, (prev) => ({ ...prev, error: undefined, @@ -2890,33 +2911,44 @@ export class RouterCore< } loadRouteChunk = (route: AnyRoute) => { - if (route._lazyPromise === undefined) { + if (!route._lazyLoaded && route._lazyPromise === undefined) { if (route.lazyFn) { route._lazyPromise = route.lazyFn().then((lazyRoute) => { // explicitly don't copy over the lazy route's id const { id: _id, ...options } = lazyRoute.options Object.assign(route.options, options) + route._lazyLoaded = true + route._lazyPromise = undefined // gc promise, we won't need it anymore }) } else { - route._lazyPromise = Promise.resolve() + route._lazyLoaded = true } } // If for some reason lazy resolves more lazy components... - // We'll wait for that before pre attempt to preload any + // We'll wait for that before we attempt to preload the // components themselves. - if (route._componentsPromise === undefined) { - route._componentsPromise = route._lazyPromise.then(() => - Promise.all( - componentTypes.map(async (type) => { - const component = route.options[type] - if ((component as any)?.preload) { - await (component as any).preload() - } - }), - ), - ) + if (!route._componentsLoaded && route._componentsPromise === undefined) { + const loadComponents = () => { + const preloads = [] + for (const type of componentTypes) { + const preload = (route.options[type] as any)?.preload + if (preload) preloads.push(preload()) + } + if (preloads.length) + return Promise.all(preloads).then(() => { + route._componentsLoaded = true + route._componentsPromise = undefined // gc promise, we won't need it anymore + }) + route._componentsLoaded = true + route._componentsPromise = undefined // gc promise, we won't need it anymore + return + } + route._componentsPromise = route._lazyPromise + ? route._lazyPromise.then(loadComponents) + : loadComponents() } + return route._componentsPromise } diff --git a/packages/router-core/src/utils.ts b/packages/router-core/src/utils.ts index 784e7c4227..4afd82d87b 100644 --- a/packages/router-core/src/utils.ts +++ b/packages/router-core/src/utils.ts @@ -473,3 +473,13 @@ export function isModuleNotFoundError(error: any): boolean { error.message.startsWith('Importing a module script failed') ) } + +export function isPromise( + value: Promise> | T, +): value is Promise> { + return Boolean( + value && + typeof value === 'object' && + typeof (value as Promise).then === 'function', + ) +} diff --git a/packages/solid-router/src/Match.tsx b/packages/solid-router/src/Match.tsx index 5623e35954..9dc3d69f00 100644 --- a/packages/solid-router/src/Match.tsx +++ b/packages/solid-router/src/Match.tsx @@ -263,9 +263,7 @@ export const MatchInner = (props: { matchId: string }): any => { if (!router.isServer) { const minPendingPromise = createControlledPromise() - Promise.resolve().then(() => { - routerMatch._nonReactive.minPendingPromise = minPendingPromise - }) + routerMatch._nonReactive.minPendingPromise = minPendingPromise setTimeout(() => { minPendingPromise.resolve()