@@ -142,14 +142,24 @@ export class ReactRouterViewStack extends ViewStacks {
142142 // Special case: reuse tabs/* and other specific wildcard routes
143143 // Don't reuse index routes (empty path) or generic catch-all wildcards (*)
144144 if ( existingPath === routePath && existingPath !== '' && existingPath !== '*' ) {
145- // For parameterized routes (containing :param), only reuse if the ACTUAL pathname matches
146- // This ensures /details/1 and /details/2 get separate view items and component instances
145+ // Parameterized routes need pathname matching to ensure /details/1 and /details/2
146+ // get separate view items. For wildcard routes (e.g., user/:userId/*), compare
147+ // pathnameBase to allow child path changes while preserving the parent view.
147148 const hasParams = routePath . includes ( ':' ) ;
149+ const isWildcard = routePath . includes ( '*' ) ;
148150 if ( hasParams ) {
149- // Check if the existing view item's pathname matches the new pathname
150- const existingPathname = v . routeData ?. match ?. pathname ;
151- if ( existingPathname !== routeInfo . pathname ) {
152- return false ; // Different param values, don't reuse
151+ if ( isWildcard ) {
152+ const existingPathnameBase = v . routeData ?. match ?. pathnameBase ;
153+ const newMatch = matchComponent ( reactElement , routeInfo . pathname , false ) ;
154+ const newPathnameBase = newMatch ?. pathnameBase ;
155+ if ( existingPathnameBase !== newPathnameBase ) {
156+ return false ;
157+ }
158+ } else {
159+ const existingPathname = v . routeData ?. match ?. pathname ;
160+ if ( existingPathname !== routeInfo . pathname ) {
161+ return false ;
162+ }
153163 }
154164 }
155165 return true ;
@@ -339,13 +349,34 @@ export class ReactRouterViewStack extends ViewStacks {
339349 < RouteContext . Consumer key = { `view-context-${ viewItem . id } ` } >
340350 { ( parentContext ) => {
341351 const parentMatches = parentContext ?. matches ?? [ ] ;
342- const accumulatedParentParams = parentMatches . reduce < Record < string , string | string [ ] | undefined > > (
352+ let accumulatedParentParams = parentMatches . reduce < Record < string , string | string [ ] | undefined > > (
343353 ( acc , match ) => {
344354 return { ...acc , ...match . params } ;
345355 } ,
346356 { }
347357 ) ;
348358
359+ // If parentMatches is empty, try to extract params from view items in other outlets.
360+ // This handles cases where React context propagation doesn't work as expected
361+ // for nested router outlets.
362+ if ( parentMatches . length === 0 && Object . keys ( accumulatedParentParams ) . length === 0 ) {
363+ const allViewItems = this . getAllViewItems ( ) ;
364+ for ( const otherViewItem of allViewItems ) {
365+ // Skip view items from the same outlet
366+ if ( otherViewItem . outletId === viewItem . outletId ) continue ;
367+
368+ // Check if this view item's route could match the current pathname
369+ const otherMatch = otherViewItem . routeData ?. match ;
370+ if ( otherMatch && otherMatch . params && Object . keys ( otherMatch . params ) . length > 0 ) {
371+ // Check if the current pathname starts with this view item's matched pathname
372+ const matchedPathname = otherMatch . pathnameBase || otherMatch . pathname ;
373+ if ( matchedPathname && routeInfo . pathname . startsWith ( matchedPathname ) ) {
374+ accumulatedParentParams = { ...accumulatedParentParams , ...otherMatch . params } ;
375+ }
376+ }
377+ }
378+ }
379+
349380 const combinedParams = {
350381 ...accumulatedParentParams ,
351382 ...( routeMatch ?. params ?? { } ) ,
@@ -620,17 +651,27 @@ export class ReactRouterViewStack extends ViewStacks {
620651 if ( result ) {
621652 const hasParams = result . params && Object . keys ( result . params ) . length > 0 ;
622653 const isSamePath = result . pathname === previousMatch ?. pathname ;
654+ const isWildcardRoute = viewItemPath . includes ( '*' ) ;
655+ const isParameterRoute = viewItemPath . includes ( ':' ) ;
623656
624657 // Don't allow view items with undefined paths to match specific routes
625658 // This prevents broken index route view items from interfering with navigation
626659 if ( ! viewItemPath && ! isIndexRoute && pathname !== '/' && pathname !== '' ) {
627660 return false ;
628661 }
629662
630- // For parameterized routes, never reuse if the pathname is different
631- // This ensures /details/1 and /details/2 get separate view items
632- const isParameterRoute = viewItemPath . includes ( ':' ) ;
663+ // For parameterized routes, check if we should reuse the view item.
664+ // Wildcard routes (e.g., user/:userId/*) compare pathnameBase to allow
665+ // child path changes while preserving the parent view.
633666 if ( isParameterRoute && ! isSamePath ) {
667+ if ( isWildcardRoute ) {
668+ const isSameBase = result . pathnameBase === previousMatch ?. pathnameBase ;
669+ if ( isSameBase ) {
670+ match = result ;
671+ viewItem = v ;
672+ return true ;
673+ }
674+ }
634675 return false ;
635676 }
636677
@@ -642,8 +683,7 @@ export class ReactRouterViewStack extends ViewStacks {
642683 return true ;
643684 }
644685
645- // For wildcard routes, only reuse if the pathname exactly matches
646- const isWildcardRoute = viewItemPath . includes ( '*' ) ;
686+ // For wildcard routes (without params), only reuse if the pathname exactly matches
647687 if ( isWildcardRoute && isSamePath ) {
648688 match = result ;
649689 viewItem = v ;
0 commit comments