Skip to content

Commit 85f379c

Browse files
committed
refactor(router-core): flatten loadRouteMatch, can run synchronously
1 parent 40f9ab7 commit 85f379c

File tree

2 files changed

+130
-107
lines changed

2 files changed

+130
-107
lines changed

packages/router-core/src/load-matches.ts

Lines changed: 129 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createControlledPromise, isPromise } from './utils'
44
import { isNotFound } from './not-found'
55
import { rootRouteId } from './root'
66
import { isRedirect } from './redirect'
7+
import type { Awaitable } from './utils'
78
import type { NotFoundError } from './not-found'
89
import type { ParsedLocation } from './location'
910
import type {
@@ -34,7 +35,7 @@ type InnerLoadContext = {
3435
onReady?: () => Promise<void>
3536
sync?: boolean
3637
/** mutable state, scoped to a `loadMatches` call */
37-
matchPromises: Array<Promise<AnyRouteMatch>>
38+
matchPromises: Array<Awaitable<AnyRouteMatch>>
3839
}
3940

4041
const triggerOnReady = (inner: InnerLoadContext): void | Promise<void> => {
@@ -703,10 +704,10 @@ const runLoader = async (
703704
}
704705
}
705706

706-
const loadRouteMatch = async (
707+
const loadRouteMatch = (
707708
inner: InnerLoadContext,
708709
index: number,
709-
): Promise<AnyRouteMatch> => {
710+
): Awaitable<AnyRouteMatch> => {
710711
const { id: matchId, routeId } = inner.matches[index]!
711712
let loaderShouldRunAsync = false
712713
let loaderIsRunningAsync = false
@@ -716,121 +717,140 @@ const loadRouteMatch = async (
716717
if (inner.router.isServer) {
717718
const headResult = executeHead(inner, matchId, route)
718719
if (headResult) {
719-
const head = await headResult
720-
inner.updateMatch(matchId, (prev) => ({
721-
...prev,
722-
...head,
723-
}))
720+
return headResult.then((head) => {
721+
inner.updateMatch(matchId, (prev) => ({
722+
...prev,
723+
...head,
724+
}))
725+
return inner.router.getMatch(matchId)!
726+
})
724727
}
725728
return inner.router.getMatch(matchId)!
726729
}
727-
} else {
728-
const prevMatch = inner.router.getMatch(matchId)!
729-
// there is a loaderPromise, so we are in the middle of a load
730-
if (prevMatch._nonReactive.loaderPromise) {
731-
// do not block if we already have stale data we can show
732-
// but only if the ongoing load is not a preload since error handling is different for preloads
733-
// and we don't want to swallow errors
734-
if (prevMatch.status === 'success' && !inner.sync && !prevMatch.preload) {
735-
return prevMatch
736-
}
737-
await prevMatch._nonReactive.loaderPromise
730+
return settleLoadRouteMatch()
731+
}
732+
733+
const prevMatch = inner.router.getMatch(matchId)!
734+
735+
// there is a loaderPromise, so we are in the middle of a load
736+
if (prevMatch._nonReactive.loaderPromise) {
737+
// do not block if we already have stale data we can show
738+
// but only if the ongoing load is not a preload since error handling is different for preloads
739+
// and we don't want to swallow errors
740+
if (prevMatch.status === 'success' && !inner.sync && !prevMatch.preload) {
741+
return prevMatch
742+
}
743+
return prevMatch._nonReactive.loaderPromise.then(() => {
738744
const match = inner.router.getMatch(matchId)!
739745
if (match.error) {
740746
handleRedirectAndNotFound(inner, match, match.error)
741747
}
742-
} else {
743-
// This is where all of the stale-while-revalidate magic happens
744-
const age = Date.now() - prevMatch.updatedAt
745-
746-
const preload = resolvePreload(inner, matchId)
747-
748-
const staleAge = preload
749-
? (route.options.preloadStaleTime ??
750-
inner.router.options.defaultPreloadStaleTime ??
751-
30_000) // 30 seconds for preloads by default
752-
: (route.options.staleTime ??
753-
inner.router.options.defaultStaleTime ??
754-
0)
755-
756-
const shouldReloadOption = route.options.shouldReload
757-
758-
// Default to reloading the route all the time
759-
// Allow shouldReload to get the last say,
760-
// if provided.
761-
const shouldReload =
762-
typeof shouldReloadOption === 'function'
763-
? shouldReloadOption(getLoaderContext(inner, matchId, index, route))
764-
: shouldReloadOption
765-
766-
const nextPreload =
767-
!!preload && !inner.router.state.matches.some((d) => d.id === matchId)
768-
const match = inner.router.getMatch(matchId)!
769-
match._nonReactive.loaderPromise = createControlledPromise<void>()
770-
if (nextPreload !== match.preload) {
771-
inner.updateMatch(matchId, (prev) => ({
772-
...prev,
773-
preload: nextPreload,
774-
}))
775-
}
748+
return settleLoadRouteMatch()
749+
})
750+
}
751+
752+
// This is where all of the stale-while-revalidate magic happens
753+
const age = Date.now() - prevMatch.updatedAt
754+
755+
const preload = resolvePreload(inner, matchId)
756+
757+
const staleAge = preload
758+
? (route.options.preloadStaleTime ??
759+
inner.router.options.defaultPreloadStaleTime ??
760+
30_000) // 30 seconds for preloads by default
761+
: (route.options.staleTime ?? inner.router.options.defaultStaleTime ?? 0)
762+
763+
const shouldReloadOption = route.options.shouldReload
764+
765+
// Default to reloading the route all the time
766+
// Allow shouldReload to get the last say,
767+
// if provided.
768+
const shouldReload =
769+
typeof shouldReloadOption === 'function'
770+
? shouldReloadOption(getLoaderContext(inner, matchId, index, route))
771+
: shouldReloadOption
772+
773+
const nextPreload =
774+
!!preload && !inner.router.state.matches.some((d) => d.id === matchId)
775+
const match = inner.router.getMatch(matchId)!
776+
match._nonReactive.loaderPromise = createControlledPromise<void>()
777+
if (nextPreload !== match.preload) {
778+
inner.updateMatch(matchId, (prev) => ({
779+
...prev,
780+
preload: nextPreload,
781+
}))
782+
}
776783

777-
// If the route is successful and still fresh, just resolve
778-
const { status, invalid } = match
779-
loaderShouldRunAsync =
780-
status === 'success' && (invalid || (shouldReload ?? age > staleAge))
781-
if (preload && route.options.preload === false) {
782-
// Do nothing
783-
} else if (loaderShouldRunAsync && !inner.sync) {
784-
loaderIsRunningAsync = true
785-
;(async () => {
786-
try {
787-
await runLoader(inner, matchId, index, route)
788-
const match = inner.router.getMatch(matchId)!
789-
match._nonReactive.loaderPromise?.resolve()
790-
match._nonReactive.loadPromise?.resolve()
791-
match._nonReactive.loaderPromise = undefined
792-
} catch (err) {
793-
if (isRedirect(err)) {
794-
await inner.router.navigate(err.options)
795-
}
796-
}
797-
})()
798-
} else if (status !== 'success' || (loaderShouldRunAsync && inner.sync)) {
784+
if (preload && route.options.preload === false) {
785+
// Do nothing
786+
return settleLoadRouteMatch()
787+
}
788+
789+
// If the route is successful and still fresh, just resolve
790+
const { status, invalid } = match
791+
loaderShouldRunAsync =
792+
status === 'success' && (invalid || (shouldReload ?? age > staleAge))
793+
if (loaderShouldRunAsync && !inner.sync) {
794+
loaderIsRunningAsync = true
795+
;(async () => {
796+
try {
799797
await runLoader(inner, matchId, index, route)
800-
} else {
801-
// if the loader did not run, still update head.
802-
// reason: parent's beforeLoad may have changed the route context
803-
// and only now do we know the route context (and that the loader would not run)
804-
const headResult = executeHead(inner, matchId, route)
805-
if (headResult) {
806-
const head = await headResult
807-
inner.updateMatch(matchId, (prev) => ({
808-
...prev,
809-
...head,
810-
}))
798+
const match = inner.router.getMatch(matchId)!
799+
match._nonReactive.loaderPromise?.resolve()
800+
match._nonReactive.loadPromise?.resolve()
801+
match._nonReactive.loaderPromise = undefined
802+
} catch (err) {
803+
if (isRedirect(err)) {
804+
await inner.router.navigate(err.options)
811805
}
812806
}
813-
}
807+
})()
808+
return settleLoadRouteMatch()
814809
}
815-
const match = inner.router.getMatch(matchId)!
816-
if (!loaderIsRunningAsync) {
817-
match._nonReactive.loaderPromise?.resolve()
818-
match._nonReactive.loadPromise?.resolve()
810+
811+
if (status !== 'success' || (loaderShouldRunAsync && inner.sync)) {
812+
return runLoader(inner, matchId, index, route).then(settleLoadRouteMatch)
819813
}
820814

821-
clearTimeout(match._nonReactive.pendingTimeout)
822-
match._nonReactive.pendingTimeout = undefined
823-
if (!loaderIsRunningAsync) match._nonReactive.loaderPromise = undefined
824-
match._nonReactive.dehydrated = undefined
825-
const nextIsFetching = loaderIsRunningAsync ? match.isFetching : false
826-
if (nextIsFetching !== match.isFetching || match.invalid !== false) {
827-
inner.updateMatch(matchId, (prev) => ({
828-
...prev,
829-
isFetching: nextIsFetching,
830-
invalid: false,
831-
}))
832-
return inner.router.getMatch(matchId)!
833-
} else {
815+
// if the loader did not run, still update head.
816+
// reason: parent's beforeLoad may have changed the route context
817+
// and only now do we know the route context (and that the loader would not run)
818+
const headResult = executeHead(inner, matchId, route)
819+
if (headResult) {
820+
return headResult.then((head) => {
821+
inner.updateMatch(matchId, (prev) => ({
822+
...prev,
823+
...head,
824+
}))
825+
return settleLoadRouteMatch()
826+
})
827+
}
828+
829+
return settleLoadRouteMatch()
830+
831+
function settleLoadRouteMatch() {
832+
const match = inner.router.getMatch(matchId)!
833+
834+
if (!loaderIsRunningAsync) {
835+
match._nonReactive.loaderPromise?.resolve()
836+
match._nonReactive.loadPromise?.resolve()
837+
match._nonReactive.loaderPromise = undefined
838+
}
839+
840+
clearTimeout(match._nonReactive.pendingTimeout)
841+
match._nonReactive.pendingTimeout = undefined
842+
match._nonReactive.dehydrated = undefined
843+
844+
const nextIsFetching = loaderIsRunningAsync ? match.isFetching : false
845+
if (nextIsFetching !== match.isFetching || match.invalid !== false) {
846+
inner.updateMatch(matchId, (prev) => ({
847+
...prev,
848+
isFetching: nextIsFetching,
849+
invalid: false,
850+
}))
851+
return inner.router.getMatch(matchId)!
852+
}
853+
834854
return match
835855
}
836856
}
@@ -866,10 +886,13 @@ export async function loadMatches(arg: {
866886

867887
// Execute all loaders in parallel
868888
const max = inner.firstBadMatchIndex ?? inner.matches.length
889+
let hasPromises = false
869890
for (let i = 0; i < max; i++) {
870-
inner.matchPromises.push(loadRouteMatch(inner, i))
891+
const result = loadRouteMatch(inner, i)
892+
inner.matchPromises.push(result)
893+
if (!hasPromises && isPromise(result)) hasPromises = true
871894
}
872-
await Promise.all(inner.matchPromises)
895+
if (hasPromises) await Promise.all(inner.matchPromises)
873896

874897
const readyPromise = triggerOnReady(inner)
875898
if (isPromise(readyPromise)) await readyPromise

packages/router-core/src/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1236,7 +1236,7 @@ export interface LoaderFnContext<
12361236
// root route does not have a parent match
12371237
parentMatchPromise: TId extends RootRouteId
12381238
? never
1239-
: Promise<MakeRouteMatchFromRoute<TParentRoute>>
1239+
: Awaitable<MakeRouteMatchFromRoute<TParentRoute>>
12401240
cause: 'preload' | 'enter' | 'stay'
12411241
route: AnyRoute
12421242
}

0 commit comments

Comments
 (0)