@@ -29,6 +29,114 @@ const NAVIGATE_REDIRECT_DELAY_MS = 100;
2929 */
3030const VIEW_CLEANUP_DELAY_MS = 200 ;
3131
32+ type RouteParams = Record < string , string | undefined > ;
33+
34+ type RouteContextMatch = {
35+ params : RouteParams ;
36+ pathname : string ;
37+ pathnameBase : string ;
38+ route : {
39+ id : string ;
40+ path ?: string ;
41+ element : React . ReactNode ;
42+ index : boolean ;
43+ caseSensitive ?: boolean ;
44+ hasErrorBoundary : boolean ;
45+ } ;
46+ } ;
47+
48+ /**
49+ * Computes the absolute pathnameBase for a route element based on its type.
50+ * Handles relative paths, index routes, and splat routes differently.
51+ */
52+ const computeAbsolutePathnameBase = (
53+ routeElement : React . ReactElement ,
54+ routeMatch : PathMatch < string > | undefined ,
55+ parentPathnameBase : string ,
56+ routeInfoPathname : string
57+ ) : string => {
58+ const routePath = routeElement . props . path ;
59+ const isRelativePath = routePath && ! routePath . startsWith ( '/' ) ;
60+ const isIndexRoute = ! ! routeElement . props . index ;
61+ const isSplatOnlyRoute = routePath === '*' || routePath === '/*' ;
62+
63+ if ( isSplatOnlyRoute ) {
64+ // Splat routes should NOT contribute their matched portion to pathnameBase
65+ // This aligns with React Router v7's v7_relativeSplatPath behavior
66+ return parentPathnameBase ;
67+ }
68+
69+ if ( isRelativePath && routeMatch ?. pathnameBase ) {
70+ const relativeBase = routeMatch . pathnameBase . startsWith ( '/' )
71+ ? routeMatch . pathnameBase . slice ( 1 )
72+ : routeMatch . pathnameBase ;
73+ return parentPathnameBase === '/' ? `/${ relativeBase } ` : `${ parentPathnameBase } /${ relativeBase } ` ;
74+ }
75+
76+ if ( isIndexRoute ) {
77+ return parentPathnameBase ;
78+ }
79+
80+ return routeMatch ?. pathnameBase || routeInfoPathname ;
81+ } ;
82+
83+ /**
84+ * Gets fallback params from view items in other outlets when parent context is empty.
85+ * This handles cases where React context propagation doesn't work as expected.
86+ */
87+ const getFallbackParamsFromViewItems = (
88+ allViewItems : ViewItem [ ] ,
89+ currentOutletId : string ,
90+ currentPathname : string
91+ ) : RouteParams => {
92+ const params : RouteParams = { } ;
93+
94+ for ( const otherViewItem of allViewItems ) {
95+ if ( otherViewItem . outletId === currentOutletId ) continue ;
96+
97+ const otherMatch = otherViewItem . routeData ?. match ;
98+ if ( otherMatch ?. params && Object . keys ( otherMatch . params ) . length > 0 ) {
99+ const matchedPathname = otherMatch . pathnameBase || otherMatch . pathname ;
100+ if ( matchedPathname && currentPathname . startsWith ( matchedPathname ) ) {
101+ Object . assign ( params , otherMatch . params ) ;
102+ }
103+ }
104+ }
105+
106+ return params ;
107+ } ;
108+
109+ /**
110+ * Builds the matches array for RouteContext.
111+ */
112+ const buildContextMatches = (
113+ parentMatches : RouteContextMatch [ ] ,
114+ combinedParams : RouteParams ,
115+ routeMatch : PathMatch < string > | undefined ,
116+ routeInfoPathname : string ,
117+ absolutePathnameBase : string ,
118+ viewItem : ViewItem ,
119+ routeElement : React . ReactElement ,
120+ componentElement : React . ReactNode
121+ ) : RouteContextMatch [ ] => {
122+ return [
123+ ...parentMatches ,
124+ {
125+ params : combinedParams ,
126+ pathname : routeMatch ?. pathname || routeInfoPathname ,
127+ pathnameBase : absolutePathnameBase ,
128+ route : {
129+ id : viewItem . id ,
130+ path : routeElement . props . path ,
131+ element : componentElement ,
132+ index : ! ! routeElement . props . index ,
133+ caseSensitive : routeElement . props . caseSensitive ,
134+ hasErrorBoundary : false ,
135+ } ,
136+ } ,
137+ ] ;
138+ } ;
139+
32140const createDefaultMatch = (
33141 fullPathname : string ,
34142 routeProps : { path ?: string ; caseSensitive ?: boolean ; end ?: boolean ; index ?: boolean }
@@ -345,103 +453,42 @@ export class ReactRouterViewStack extends ViewStacks {
345453 return (
346454 < RouteContext . Consumer key = { `view-context-${ viewItem . id } ` } >
347455 { ( parentContext ) => {
348- const parentMatches = parentContext ?. matches ?? [ ] ;
349- let accumulatedParentParams = parentMatches . reduce < Record < string , string | string [ ] | undefined > > (
350- ( acc , match ) => {
351- return { ...acc , ...match . params } ;
352- } ,
353- { }
354- ) ;
456+ const parentMatches = ( parentContext ?. matches ?? [ ] ) as RouteContextMatch [ ] ;
355457
356- // If parentMatches is empty, try to extract params from view items in other outlets.
357- // This handles cases where React context propagation doesn't work as expected
358- // for nested router outlets.
458+ // Accumulate params from parent matches, with fallback to other outlets
459+ let accumulatedParentParams = parentMatches . reduce < RouteParams > ( ( acc , m ) => ( { ...acc , ...m . params } ) , { } ) ;
359460 if ( parentMatches . length === 0 && Object . keys ( accumulatedParentParams ) . length === 0 ) {
360- const allViewItems = this . getAllViewItems ( ) ;
361- for ( const otherViewItem of allViewItems ) {
362- // Skip view items from the same outlet
363- if ( otherViewItem . outletId === viewItem . outletId ) continue ;
364-
365- // Check if this view item's route could match the current pathname
366- const otherMatch = otherViewItem . routeData ?. match ;
367- if ( otherMatch && otherMatch . params && Object . keys ( otherMatch . params ) . length > 0 ) {
368- // Check if the current pathname starts with this view item's matched pathname
369- const matchedPathname = otherMatch . pathnameBase || otherMatch . pathname ;
370- if ( matchedPathname && routeInfo . pathname . startsWith ( matchedPathname ) ) {
371- accumulatedParentParams = { ...accumulatedParentParams , ...otherMatch . params } ;
372- }
373- }
374- }
461+ accumulatedParentParams = getFallbackParamsFromViewItems (
462+ this . getAllViewItems ( ) ,
463+ viewItem . outletId ,
464+ routeInfo . pathname
465+ ) ;
375466 }
376467
377- const combinedParams = {
378- ...accumulatedParentParams ,
379- ...( routeMatch ?. params ?? { } ) ,
380- } ;
381-
382- // For relative route paths, we need to compute an absolute pathnameBase
383- // by combining the parent's pathnameBase with the matched portion
384- const routePath = routeElement . props . path ;
385- const isRelativePath = routePath && ! routePath . startsWith ( '/' ) ;
386- const isIndexRoute = ! ! routeElement . props . index ;
387- const isSplatOnlyRoute = routePath === '*' || routePath === '/*' ;
388-
389- // Get parent's pathnameBase for relative path resolution
468+ const combinedParams = { ...accumulatedParentParams , ...( routeMatch ?. params ?? { } ) } ;
390469 const parentPathnameBase =
391470 parentMatches . length > 0 ? parentMatches [ parentMatches . length - 1 ] . pathnameBase : '/' ;
471+ const absolutePathnameBase = computeAbsolutePathnameBase (
472+ routeElement ,
473+ routeMatch ,
474+ parentPathnameBase ,
475+ routeInfo . pathname
476+ ) ;
392477
393- // Start with the match's pathnameBase, falling back to routeInfo.pathname
394- // BUT: splat-only routes should use parent's base (v7_relativeSplatPath behavior)
395- let absolutePathnameBase : string ;
396-
397- if ( isSplatOnlyRoute ) {
398- // Splat routes should NOT contribute their matched portion to pathnameBase
399- // This aligns with React Router v7's v7_relativeSplatPath behavior
400- // Without this, relative links inside splat routes get double path segments
401- absolutePathnameBase = parentPathnameBase ;
402- } else if ( isRelativePath && routeMatch ?. pathnameBase ) {
403- // For relative paths with a pathnameBase, combine with parent
404- const relativeBase = routeMatch . pathnameBase . startsWith ( '/' )
405- ? routeMatch . pathnameBase . slice ( 1 )
406- : routeMatch . pathnameBase ;
407-
408- absolutePathnameBase =
409- parentPathnameBase === '/' ? `/${ relativeBase } ` : `${ parentPathnameBase } /${ relativeBase } ` ;
410- } else if ( isIndexRoute ) {
411- // Index routes should use the parent's base as their base
412- absolutePathnameBase = parentPathnameBase ;
413- } else {
414- // Default: use the match's pathnameBase or the current pathname
415- absolutePathnameBase = routeMatch ?. pathnameBase || routeInfo . pathname ;
416- }
417-
418- const contextMatches = [
419- ...parentMatches ,
420- {
421- params : combinedParams ,
422- pathname : routeMatch ?. pathname || routeInfo . pathname ,
423- pathnameBase : absolutePathnameBase ,
424- route : {
425- id : viewItem . id ,
426- path : routeElement . props . path ,
427- element : componentElement ,
428- index : ! ! routeElement . props . index ,
429- caseSensitive : routeElement . props . caseSensitive ,
430- hasErrorBoundary : false ,
431- } ,
432- } ,
433- ] ;
478+ const contextMatches = buildContextMatches (
479+ parentMatches ,
480+ combinedParams ,
481+ routeMatch ,
482+ routeInfo . pathname ,
483+ absolutePathnameBase ,
484+ viewItem ,
485+ routeElement ,
486+ componentElement
487+ ) ;
434488
435489 const routeContextValue = parentContext
436- ? {
437- ...parentContext ,
438- matches : contextMatches ,
439- }
440- : {
441- outlet : null ,
442- matches : contextMatches ,
443- isDataRoute : false ,
444- } ;
490+ ? { ...parentContext , matches : contextMatches }
491+ : { outlet : null , matches : contextMatches , isDataRoute : false } ;
445492
446493 return (
447494 < ViewLifeCycleManager
0 commit comments