@@ -20,7 +20,25 @@ import { renderRouteNotFound } from './renderRouteNotFound'
2020import { ScrollRestoration } from './scroll-restoration'
2121import { ClientOnly } from './ClientOnly'
2222import { 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
2543export 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