Skip to content

refactor(router-core): skip beforeLoad/loader store updates if these options aren't provided #4920

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,6 @@ describe('Store updates 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(after - before).toBe(19)
expect(after - before).toBe(14)
})
})
183 changes: 114 additions & 69 deletions packages/router-core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2299,6 +2299,20 @@ export class RouterCore<
continue
}

const handleSearchAndParamSerialErrors = (
index: number,
matchId: string,
) => {
const { paramsError, searchError } = this.getMatch(matchId)!
if (paramsError) {
handleSerialError(index, paramsError, 'PARSE_PARAMS')
}

if (searchError) {
handleSerialError(index, searchError, 'VALIDATE_SEARCH')
}
}

const shouldPending = !!(
onReady &&
!this.isServer &&
Expand All @@ -2311,8 +2325,6 @@ export class RouterCore<
(route.options.pendingComponent ??
(this.options as any)?.defaultPendingComponent)
)

let executeBeforeLoad = true
const setupPendingTimeout = () => {
const match = this.getMatch(matchId)!
if (
Expand All @@ -2329,14 +2341,35 @@ export class RouterCore<
match._nonReactive.pendingTimeout = pendingTimeout
}
}
if (
// If we are in the middle of a load, either of these will be present
// (not to be confused with `loadPromise`, which is always defined)

// If we are in the middle of a load, either of these will be present
// (not to be confused with `loadPromise`, which is always defined)
const hasBeforeLoadOrLoaderPromise =
existingMatch._nonReactive.beforeLoadPromise ||
existingMatch._nonReactive.loaderPromise
) {

if (hasBeforeLoadOrLoaderPromise) {
setupPendingTimeout()
}

// we can bail out early if there is no `beforeLoad`
if (!route.options.beforeLoad) {
handleSearchAndParamSerialErrors(index, matchId)
const parentMatchContext =
parentMatch?.context ?? this.options.context ?? {}
updateMatch(matchId, (prev) => ({
...prev,
context: {
...parentMatchContext,
...prev.__routeContext,
},
}))
continue
}

let executeBeforeLoad = true

if (hasBeforeLoadOrLoaderPromise) {
// Wait for the beforeLoad to resolve before we continue
await existingMatch._nonReactive.beforeLoadPromise
const match = this.getMatch(matchId)!
Expand All @@ -2349,6 +2382,10 @@ export class RouterCore<
handleRedirectAndNotFound(match, match.error)
}
}

let beforeLoadContext =
this.getMatch(matchId)!.__beforeLoadContext

if (executeBeforeLoad) {
// If we are not in the middle of a load OR the previous load failed, start it
try {
Expand All @@ -2362,15 +2399,7 @@ export class RouterCore<
prevLoadPromise?.resolve()
})

const { paramsError, searchError } = this.getMatch(matchId)!

if (paramsError) {
handleSerialError(index, paramsError, 'PARSE_PARAMS')
}

if (searchError) {
handleSerialError(index, searchError, 'VALIDATE_SEARCH')
}
handleSearchAndParamSerialErrors(index, matchId)

setupPendingTimeout()

Expand Down Expand Up @@ -2415,29 +2444,24 @@ export class RouterCore<
matches,
}

const beforeLoadContext =
await route.options.beforeLoad?.(beforeLoadFnContext)
beforeLoadContext =
await route.options.beforeLoad!(beforeLoadFnContext)

if (
isRedirect(beforeLoadContext) ||
isNotFound(beforeLoadContext)
) {
handleSerialError(index, beforeLoadContext, 'BEFORE_LOAD')
}

updateMatch(matchId, (prev) => {
return {
...prev,
__beforeLoadContext: beforeLoadContext,
context: {
...parentMatchContext,
...prev.__routeContext,
...beforeLoadContext,
},
abortController,
}
})
} catch (err) {
updateMatch(matchId, (prev) => ({
...prev,
__beforeLoadContext: beforeLoadContext,
context: {
...prev.context,
...beforeLoadContext,
},
}))
handleSerialError(index, err, 'BEFORE_LOAD')
}

Expand All @@ -2447,7 +2471,12 @@ export class RouterCore<

return {
...prev,
__beforeLoadContext: beforeLoadContext,
isFetching: false,
context: {
...prev.context,
...beforeLoadContext,
},
}
})
}
Expand All @@ -2469,21 +2498,33 @@ export class RouterCore<
if (!match) {
return
}
if (
!route.options.head &&
!route.options.scripts &&
!route.options.headers
) {
return
}
const assetContext = {
matches,
match,
params: match.params,
loaderData: match.loaderData,
}
const headFnContent =
await route.options.head?.(assetContext)

const [headFnContent, scripts, headers] = await Promise.all(
[
route.options.head?.(assetContext),
route.options.scripts?.(assetContext),
route.options.headers?.(assetContext),
],
)

const meta = headFnContent?.meta
const links = headFnContent?.links
const headScripts = headFnContent?.scripts
const styles = headFnContent?.styles

const scripts = await route.options.scripts?.(assetContext)
const headers = await route.options.headers?.(assetContext)
return {
meta,
links,
Expand All @@ -2494,21 +2535,21 @@ 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)!
if (shouldSkipLoader(matchId)) {
if (this.isServer) {
const head = await executeHead()
updateMatch(matchId, (prev) => ({
...prev,
...head,
}))
if (head) {
updateMatch(matchId, (prev) => ({
...prev,
...head,
}))
}
return this.getMatch(matchId)!
}
}
Expand Down Expand Up @@ -2604,29 +2645,30 @@ 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',
}))
if (route.options.loader) {
updateMatch(matchId, (prev) => ({
...prev,
isFetching: 'loader',
}))

// Kick off the loader!
const loaderData =
await route.options.loader?.(getLoaderContext())
// Kick off the loader!
const loaderData =
await route.options.loader(getLoaderContext())

handleRedirectAndNotFound(
this.getMatch(matchId)!,
loaderData,
)
updateMatch(matchId, (prev) => ({
...prev,
loaderData,
}))
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
Expand Down Expand Up @@ -2674,14 +2716,15 @@ export class RouterCore<
} catch (err) {
const head = await executeHead()

updateMatch(matchId, (prev) => {
prev._nonReactive.loaderPromise = undefined
return {
if (head) {
updateMatch(matchId, (prev) => ({
...prev,
...head,
}
})
handleRedirectAndNotFound(this.getMatch(matchId)!, err)
}))
}
const match = this.getMatch(matchId)!
match._nonReactive.loaderPromise = undefined
handleRedirectAndNotFound(match, err)
}
}

Expand Down Expand Up @@ -2717,10 +2760,12 @@ export class RouterCore<
// 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 head = await executeHead()
updateMatch(matchId, (prev) => ({
...prev,
...head,
}))
if (head) {
updateMatch(matchId, (prev) => ({
...prev,
...head,
}))
}
}
}
if (!loaderIsRunningAsync) {
Expand Down
5 changes: 2 additions & 3 deletions packages/solid-router/tests/Transitioner.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,12 @@ describe('Transitioner', () => {
// Mock router.load() to verify it gets called
const loadSpy = vi.spyOn(router, 'load')

await router.load()

render(() => <RouterProvider router={router} />)
await router.latestLoadPromise

// Wait for the createRenderEffect to run and call router.load()
await waitFor(() => {
expect(loadSpy).toHaveBeenCalledTimes(2)
expect(loadSpy).toHaveBeenCalledTimes(1)
expect(loader).toHaveBeenCalledTimes(1)
})

Expand Down
Loading