Skip to content

Commit 56bd271

Browse files
authored
perf(react-router): add match selector compares (#7596)
* perf(react-router): add match selector compares * fix scroll restoration * Revert "fix scroll restoration" This reverts commit 620e967. * fix scroll restoration * lint rules of hooks
1 parent 52db703 commit 56bd271

1 file changed

Lines changed: 52 additions & 15 deletions

File tree

packages/react-router/src/Match.tsx

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,25 @@ import { renderRouteNotFound } from './renderRouteNotFound'
2020
import { ScrollRestoration } from './scroll-restoration'
2121
import { ClientOnly } from './ClientOnly'
2222
import { useLayoutEffect } from './utils'
23-
import type { AnyRoute, RootRouteOptions } from '@tanstack/router-core'
23+
import type {
24+
AnyRoute,
25+
AnyRouteMatch,
26+
ParsedLocation,
27+
RootRouteOptions,
28+
} from '@tanstack/router-core'
29+
30+
type OutletMatchSelection = [
31+
routeId: string | undefined,
32+
parentGlobalNotFound: boolean,
33+
]
34+
35+
const matchViewFieldsEqual = (a: AnyRouteMatch, b: AnyRouteMatch) =>
36+
a.routeId === b.routeId && a._displayPending === b._displayPending
37+
38+
const outletMatchSelectionEqual = (
39+
a: OutletMatchSelection,
40+
b: OutletMatchSelection,
41+
) => a[0] === b[0] && a[1] === b[1]
2442

2543
export const Match = React.memo(function MatchImpl({
2644
matchId,
@@ -77,7 +95,7 @@ export const Match = React.memo(function MatchImpl({
7795
// eslint-disable-next-line react-hooks/rules-of-hooks
7896
const resetKey = useStore(router.stores.loadedAt, (loadedAt) => loadedAt)
7997
// eslint-disable-next-line react-hooks/rules-of-hooks
80-
const match = useStore(matchStore, (value) => value)
98+
const match = useStore(matchStore, (value) => value, matchViewFieldsEqual)
8199
// eslint-disable-next-line react-hooks/rules-of-hooks
82100
const matchState = React.useMemo(() => {
83101
const routeId = match.routeId as string
@@ -208,7 +226,7 @@ function MatchView({
208226
</matchContext.Provider>
209227
{matchState.parentRouteId === rootRouteId ? (
210228
<>
211-
<OnRendered resetKey={resetKey} />
229+
<OnRendered />
212230
{router.options.scrollRestoration && (isServer ?? router.isServer) ? (
213231
<ScrollRestoration />
214232
) : null}
@@ -222,34 +240,49 @@ function MatchView({
222240
// the route subtree has committed below the root layout. Keeping it here lets
223241
// us fire onRendered even after a hydration mismatch above the root layout
224242
// (like bad head/link tags, which is common).
225-
function OnRendered({ resetKey }: { resetKey: number }) {
243+
function OnRendered() {
226244
const router = useRouter()
227245

228246
if (isServer ?? router.isServer) {
229247
return null
230248
}
231249

250+
// Track the resolvedLocation as of the last render so that onRendered can
251+
// report the correct fromLocation. By the time this effect fires,
252+
// resolvedLocation has already been updated to the new location by
253+
// Transitioner, so we cannot use router.stores.resolvedLocation.get()
254+
// directly as the fromLocation.
255+
// @ts-expect-error -- init to `undefined` but don't write `undefined` to shave bytes
256+
// eslint-disable-next-line react-hooks/rules-of-hooks
257+
const prevResolvedLocationRef = React.useRef<
258+
ParsedLocation<any> | undefined
259+
>()
232260
// eslint-disable-next-line react-hooks/rules-of-hooks
233-
const prevHrefRef = React.useRef<string | undefined>(undefined)
261+
const renderedLocationKey = useStore(
262+
router.stores.resolvedLocation,
263+
(resolvedLocation) => resolvedLocation?.state.__TSR_key,
264+
)
234265

235266
// eslint-disable-next-line react-hooks/rules-of-hooks
236267
useLayoutEffect(() => {
237-
const currentHref = router.latestLocation.href
268+
const currentResolvedLocation = router.stores.resolvedLocation.get()
269+
const previousResolvedLocation = prevResolvedLocationRef.current
238270

239271
if (
240-
prevHrefRef.current === undefined ||
241-
prevHrefRef.current !== currentHref
272+
currentResolvedLocation &&
273+
(!previousResolvedLocation ||
274+
previousResolvedLocation.href !== currentResolvedLocation.href)
242275
) {
243276
router.emit({
244277
type: 'onRendered',
245278
...getLocationChangeInfo(
246279
router.stores.location.get(),
247-
router.stores.resolvedLocation.get(),
280+
previousResolvedLocation ?? currentResolvedLocation,
248281
),
249282
})
250-
prevHrefRef.current = currentHref
251283
}
252-
}, [router.latestLocation.state.__TSR_key, resetKey, router])
284+
prevResolvedLocationRef.current = currentResolvedLocation
285+
}, [renderedLocationKey, router])
253286

254287
return null
255288
}
@@ -521,10 +554,14 @@ export const Outlet = React.memo(function OutletImpl() {
521554
: undefined
522555

523556
// eslint-disable-next-line react-hooks/rules-of-hooks
524-
;[routeId, parentGlobalNotFound] = useStore(parentMatchStore, (match) => [
525-
match?.routeId as string | undefined,
526-
match?.globalNotFound ?? false,
527-
])
557+
;[routeId, parentGlobalNotFound] = useStore(
558+
parentMatchStore,
559+
(match): OutletMatchSelection => [
560+
match?.routeId as string | undefined,
561+
match?.globalNotFound ?? false,
562+
],
563+
outletMatchSelectionEqual,
564+
)
528565

529566
// eslint-disable-next-line react-hooks/rules-of-hooks
530567
childMatchId = useStore(router.stores.matchesId, (ids) => {

0 commit comments

Comments
 (0)