Skip to content

Commit f9db9e3

Browse files
Sheraffautofix-ci[bot]
authored andcommitted
refactor(router-core): head, scripts, headers are executed in parallel and can skip store update (#4925)
Following #4916, this PR keeps reducing the number of store updates (`__store.setState` through `updateMatch`). We can skip `route.options.head()`, `route.options.scripts()`, and `route.options.headers()` calls, if they are not defined, and not even call `updateMatch` after. Additionally, `head`, `scripts` and `headers` are now called in parallel. They don't depend on one another, and so there is no reason to delay their execution by calling them serially. --- This PR also fixes 2 issues: - in `cancelMatch`, we were resetting the timeout id to `undefined` **before** calling `clearTimeout()` with it - `handleRedirectAndNotFound` is sometimes called w/ `match` being `undefined` (in case of a redirecting match during preload), which went undetected because of heavy `match!` assertions, and the many `try/catch` blocks --- With this PR, the `redirection in preload` test in `store-updates-during-navigation` goes from **11 updates** during the preload to **8 updates**. --- Should be a partial improvement of #4359 --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 760fc63 commit f9db9e3

File tree

2 files changed

+110
-71
lines changed

2 files changed

+110
-71
lines changed

packages/react-router/tests/store-updates-during-navigation.test.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ afterEach(() => {
1616
cleanup()
1717
})
1818

19-
async function setupAndRun({
19+
function setup({
2020
beforeLoad,
2121
loader,
2222
head,
@@ -78,6 +78,10 @@ async function setupAndRun({
7878

7979
render(<RouterProvider router={router} />)
8080

81+
return { select, router }
82+
}
83+
84+
async function run({ select }: ReturnType<typeof setup>, opts?: {}) {
8185
// navigate to /posts
8286
const link = await waitFor(() => screen.getByRole('link', { name: 'Posts' }))
8387
const before = select.mock.calls.length
@@ -93,30 +97,37 @@ async function setupAndRun({
9397

9498
describe("Store doesn't update *too many* times during navigation", () => {
9599
test('async loader, async beforeLoad, pendingMs', async () => {
96-
const updates = await setupAndRun({
100+
const params = setup({
97101
beforeLoad: () =>
98102
new Promise<void>((resolve) => setTimeout(resolve, 100)),
99103
loader: () => new Promise<void>((resolve) => setTimeout(resolve, 100)),
100104
defaultPendingMs: 100,
101105
defaultPendingMinMs: 300,
102106
})
103107

108+
const updates = await run(params)
109+
104110
// This number should be as small as possible to minimize the amount of work
105111
// that needs to be done during a navigation.
106112
// Any change that increases this number should be investigated.
107113
expect(updates).toBe(19)
108114
})
109115

110-
test('redirection', async () => {
111-
const updates = await setupAndRun({
112-
beforeLoad: () => {
116+
test('redirection in preload', async () => {
117+
const { select, router } = setup({
118+
loader: () => {
113119
throw redirect({ to: '/other' })
114120
},
115121
})
116122

123+
const before = select.mock.calls.length
124+
await router.preloadRoute({ to: '/posts' })
125+
const after = select.mock.calls.length
126+
const updates = after - before
127+
117128
// This number should be as small as possible to minimize the amount of work
118129
// that needs to be done during a navigation.
119130
// Any change that increases this number should be investigated.
120-
expect(updates).toBe(26)
131+
expect(updates).toBe(8)
121132
})
122133
})

packages/router-core/src/router.ts

Lines changed: 93 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,8 +1386,8 @@ export class RouterCore<
13861386
if (!match) return
13871387

13881388
match.abortController.abort()
1389-
match._nonReactive.pendingTimeout = undefined
13901389
clearTimeout(match._nonReactive.pendingTimeout)
1390+
match._nonReactive.pendingTimeout = undefined
13911391
}
13921392

13931393
cancelMatches = () => {
@@ -2114,7 +2114,10 @@ export class RouterCore<
21142114
triggerOnReady()
21152115
}
21162116

2117-
const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => {
2117+
const handleRedirectAndNotFound = (
2118+
match: AnyRouteMatch | undefined,
2119+
err: any,
2120+
) => {
21182121
if (isRedirect(err) || isNotFound(err)) {
21192122
if (isRedirect(err)) {
21202123
if (err.redirectHandled) {
@@ -2124,27 +2127,30 @@ export class RouterCore<
21242127
}
21252128
}
21262129

2127-
match._nonReactive.beforeLoadPromise?.resolve()
2128-
match._nonReactive.loaderPromise?.resolve()
2129-
match._nonReactive.beforeLoadPromise = undefined
2130-
match._nonReactive.loaderPromise = undefined
2131-
2132-
updateMatch(match.id, (prev) => ({
2133-
...prev,
2134-
status: isRedirect(err)
2135-
? 'redirected'
2136-
: isNotFound(err)
2137-
? 'notFound'
2138-
: 'error',
2139-
isFetching: false,
2140-
error: err,
2141-
}))
2130+
// in case of a redirecting match during preload, the match does not exist
2131+
if (match) {
2132+
match._nonReactive.beforeLoadPromise?.resolve()
2133+
match._nonReactive.loaderPromise?.resolve()
2134+
match._nonReactive.beforeLoadPromise = undefined
2135+
match._nonReactive.loaderPromise = undefined
2136+
2137+
updateMatch(match.id, (prev) => ({
2138+
...prev,
2139+
status: isRedirect(err)
2140+
? 'redirected'
2141+
: isNotFound(err)
2142+
? 'notFound'
2143+
: 'error',
2144+
isFetching: false,
2145+
error: err,
2146+
}))
21422147

2143-
if (!(err as any).routeId) {
2144-
;(err as any).routeId = match.routeId
2145-
}
2148+
if (!(err as any).routeId) {
2149+
;(err as any).routeId = match.routeId
2150+
}
21462151

2147-
match._nonReactive.loadPromise?.resolve()
2152+
match._nonReactive.loadPromise?.resolve()
2153+
}
21482154

21492155
if (isRedirect(err)) {
21502156
rendered = true
@@ -2197,13 +2203,13 @@ export class RouterCore<
21972203

21982204
err.routerCode = routerCode
21992205
firstBadMatchIndex = firstBadMatchIndex ?? index
2200-
handleRedirectAndNotFound(this.getMatch(matchId)!, err)
2206+
handleRedirectAndNotFound(this.getMatch(matchId), err)
22012207

22022208
try {
22032209
route.options.onError?.(err)
22042210
} catch (errorHandlerErr) {
22052211
err = errorHandlerErr
2206-
handleRedirectAndNotFound(this.getMatch(matchId)!, err)
2212+
handleRedirectAndNotFound(this.getMatch(matchId), err)
22072213
}
22082214

22092215
updateMatch(matchId, (prev) => {
@@ -2458,35 +2464,45 @@ export class RouterCore<
24582464
let loaderIsRunningAsync = false
24592465
const route = this.looseRoutesById[routeId]!
24602466

2461-
const executeHead = async () => {
2467+
const executeHead = () => {
24622468
const match = this.getMatch(matchId)
24632469
// in case of a redirecting match during preload, the match does not exist
24642470
if (!match) {
24652471
return
24662472
}
2473+
if (
2474+
!route.options.head &&
2475+
!route.options.scripts &&
2476+
!route.options.headers
2477+
) {
2478+
return
2479+
}
24672480
const assetContext = {
24682481
matches,
24692482
match,
24702483
params: match.params,
24712484
loaderData: match.loaderData,
24722485
}
2473-
const headFnContent =
2474-
await route.options.head?.(assetContext)
2475-
const meta = headFnContent?.meta
2476-
const links = headFnContent?.links
2477-
const headScripts = headFnContent?.scripts
2478-
const styles = headFnContent?.styles
2479-
2480-
const scripts = await route.options.scripts?.(assetContext)
2481-
const headers = await route.options.headers?.(assetContext)
2482-
return {
2483-
meta,
2484-
links,
2485-
headScripts,
2486-
headers,
2487-
scripts,
2488-
styles,
2489-
}
2486+
2487+
return Promise.all([
2488+
route.options.head?.(assetContext),
2489+
route.options.scripts?.(assetContext),
2490+
route.options.headers?.(assetContext),
2491+
]).then(([headFnContent, scripts, headers]) => {
2492+
const meta = headFnContent?.meta
2493+
const links = headFnContent?.links
2494+
const headScripts = headFnContent?.scripts
2495+
const styles = headFnContent?.styles
2496+
2497+
return {
2498+
meta,
2499+
links,
2500+
headScripts,
2501+
headers,
2502+
scripts,
2503+
styles,
2504+
}
2505+
})
24902506
}
24912507

24922508
const potentialPendingMinPromise = async () => {
@@ -2499,11 +2515,14 @@ export class RouterCore<
24992515
const prevMatch = this.getMatch(matchId)!
25002516
if (shouldSkipLoader(matchId)) {
25012517
if (this.isServer) {
2502-
const head = await executeHead()
2503-
updateMatch(matchId, (prev) => ({
2504-
...prev,
2505-
...head,
2506-
}))
2518+
const headResult = executeHead()
2519+
if (headResult) {
2520+
const head = await headResult
2521+
updateMatch(matchId, (prev) => ({
2522+
...prev,
2523+
...head,
2524+
}))
2525+
}
25072526
return this.getMatch(matchId)!
25082527
}
25092528
}
@@ -2615,7 +2634,7 @@ export class RouterCore<
26152634
await route.options.loader?.(getLoaderContext())
26162635

26172636
handleRedirectAndNotFound(
2618-
this.getMatch(matchId)!,
2637+
this.getMatch(matchId),
26192638
loaderData,
26202639
)
26212640
updateMatch(matchId, (prev) => ({
@@ -2627,7 +2646,8 @@ export class RouterCore<
26272646
// so we need to wait for it to resolve before
26282647
// we can use the options
26292648
await route._lazyPromise
2630-
const head = await executeHead()
2649+
const headResult = executeHead()
2650+
const head = headResult ? await headResult : undefined
26312651
await potentialPendingMinPromise()
26322652

26332653
// Last but not least, wait for the the components
@@ -2646,18 +2666,19 @@ export class RouterCore<
26462666

26472667
await potentialPendingMinPromise()
26482668

2649-
handleRedirectAndNotFound(this.getMatch(matchId)!, e)
2669+
handleRedirectAndNotFound(this.getMatch(matchId), e)
26502670

26512671
try {
26522672
route.options.onError?.(e)
26532673
} catch (onErrorError) {
26542674
error = onErrorError
26552675
handleRedirectAndNotFound(
2656-
this.getMatch(matchId)!,
2676+
this.getMatch(matchId),
26572677
onErrorError,
26582678
)
26592679
}
2660-
const head = await executeHead()
2680+
const headResult = executeHead()
2681+
const head = headResult ? await headResult : undefined
26612682
updateMatch(matchId, (prev) => ({
26622683
...prev,
26632684
error,
@@ -2667,16 +2688,20 @@ export class RouterCore<
26672688
}))
26682689
}
26692690
} catch (err) {
2670-
const head = await executeHead()
2671-
2672-
updateMatch(matchId, (prev) => {
2673-
prev._nonReactive.loaderPromise = undefined
2674-
return {
2675-
...prev,
2676-
...head,
2691+
const match = this.getMatch(matchId)
2692+
// in case of a redirecting match during preload, the match does not exist
2693+
if (match) {
2694+
const headResult = executeHead()
2695+
if (headResult) {
2696+
const head = await headResult
2697+
updateMatch(matchId, (prev) => ({
2698+
...prev,
2699+
...head,
2700+
}))
26772701
}
2678-
})
2679-
handleRedirectAndNotFound(this.getMatch(matchId)!, err)
2702+
match._nonReactive.loaderPromise = undefined
2703+
}
2704+
handleRedirectAndNotFound(match, err)
26802705
}
26812706
}
26822707

@@ -2711,11 +2736,14 @@ export class RouterCore<
27112736
// if the loader did not run, still update head.
27122737
// reason: parent's beforeLoad may have changed the route context
27132738
// and only now do we know the route context (and that the loader would not run)
2714-
const head = await executeHead()
2715-
updateMatch(matchId, (prev) => ({
2716-
...prev,
2717-
...head,
2718-
}))
2739+
const headResult = executeHead()
2740+
if (headResult) {
2741+
const head = await headResult
2742+
updateMatch(matchId, (prev) => ({
2743+
...prev,
2744+
...head,
2745+
}))
2746+
}
27192747
}
27202748
}
27212749
if (!loaderIsRunningAsync) {

0 commit comments

Comments
 (0)