diff --git a/packages/react-router/src/Match.tsx b/packages/react-router/src/Match.tsx
index fefd256bf6..5de5546aeb 100644
--- a/packages/react-router/src/Match.tsx
+++ b/packages/react-router/src/Match.tsx
@@ -20,7 +20,25 @@ import { renderRouteNotFound } from './renderRouteNotFound'
import { ScrollRestoration } from './scroll-restoration'
import { ClientOnly } from './ClientOnly'
import { useLayoutEffect } from './utils'
-import type { AnyRoute, RootRouteOptions } from '@tanstack/router-core'
+import type {
+ AnyRoute,
+ AnyRouteMatch,
+ ParsedLocation,
+ RootRouteOptions,
+} from '@tanstack/router-core'
+
+type OutletMatchSelection = [
+ routeId: string | undefined,
+ parentGlobalNotFound: boolean,
+]
+
+const matchViewFieldsEqual = (a: AnyRouteMatch, b: AnyRouteMatch) =>
+ a.routeId === b.routeId && a._displayPending === b._displayPending
+
+const outletMatchSelectionEqual = (
+ a: OutletMatchSelection,
+ b: OutletMatchSelection,
+) => a[0] === b[0] && a[1] === b[1]
export const Match = React.memo(function MatchImpl({
matchId,
@@ -77,7 +95,7 @@ export const Match = React.memo(function MatchImpl({
// eslint-disable-next-line react-hooks/rules-of-hooks
const resetKey = useStore(router.stores.loadedAt, (loadedAt) => loadedAt)
// eslint-disable-next-line react-hooks/rules-of-hooks
- const match = useStore(matchStore, (value) => value)
+ const match = useStore(matchStore, (value) => value, matchViewFieldsEqual)
// eslint-disable-next-line react-hooks/rules-of-hooks
const matchState = React.useMemo(() => {
const routeId = match.routeId as string
@@ -208,7 +226,7 @@ function MatchView({
{matchState.parentRouteId === rootRouteId ? (
<>
-
+
{router.options.scrollRestoration && (isServer ?? router.isServer) ? (
) : null}
@@ -222,34 +240,49 @@ function MatchView({
// the route subtree has committed below the root layout. Keeping it here lets
// us fire onRendered even after a hydration mismatch above the root layout
// (like bad head/link tags, which is common).
-function OnRendered({ resetKey }: { resetKey: number }) {
+function OnRendered() {
const router = useRouter()
if (isServer ?? router.isServer) {
return null
}
+ // Track the resolvedLocation as of the last render so that onRendered can
+ // report the correct fromLocation. By the time this effect fires,
+ // resolvedLocation has already been updated to the new location by
+ // Transitioner, so we cannot use router.stores.resolvedLocation.get()
+ // directly as the fromLocation.
+ // @ts-expect-error -- init to `undefined` but don't write `undefined` to shave bytes
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const prevResolvedLocationRef = React.useRef<
+ ParsedLocation | undefined
+ >()
// eslint-disable-next-line react-hooks/rules-of-hooks
- const prevHrefRef = React.useRef(undefined)
+ const renderedLocationKey = useStore(
+ router.stores.resolvedLocation,
+ (resolvedLocation) => resolvedLocation?.state.__TSR_key,
+ )
// eslint-disable-next-line react-hooks/rules-of-hooks
useLayoutEffect(() => {
- const currentHref = router.latestLocation.href
+ const currentResolvedLocation = router.stores.resolvedLocation.get()
+ const previousResolvedLocation = prevResolvedLocationRef.current
if (
- prevHrefRef.current === undefined ||
- prevHrefRef.current !== currentHref
+ currentResolvedLocation &&
+ (!previousResolvedLocation ||
+ previousResolvedLocation.href !== currentResolvedLocation.href)
) {
router.emit({
type: 'onRendered',
...getLocationChangeInfo(
router.stores.location.get(),
- router.stores.resolvedLocation.get(),
+ previousResolvedLocation ?? currentResolvedLocation,
),
})
- prevHrefRef.current = currentHref
}
- }, [router.latestLocation.state.__TSR_key, resetKey, router])
+ prevResolvedLocationRef.current = currentResolvedLocation
+ }, [renderedLocationKey, router])
return null
}
@@ -521,10 +554,14 @@ export const Outlet = React.memo(function OutletImpl() {
: undefined
// eslint-disable-next-line react-hooks/rules-of-hooks
- ;[routeId, parentGlobalNotFound] = useStore(parentMatchStore, (match) => [
- match?.routeId as string | undefined,
- match?.globalNotFound ?? false,
- ])
+ ;[routeId, parentGlobalNotFound] = useStore(
+ parentMatchStore,
+ (match): OutletMatchSelection => [
+ match?.routeId as string | undefined,
+ match?.globalNotFound ?? false,
+ ],
+ outletMatchSelectionEqual,
+ )
// eslint-disable-next-line react-hooks/rules-of-hooks
childMatchId = useStore(router.stores.matchesId, (ids) => {