Original file line number Diff line number Diff line change 1- import { createEffect , onMount , onCleanup , startTransition , useTransition , untrack } from 'solid-js' ;
2- import { useStore } from '@tanstack/solid-store' ;
1+ import { batch , createEffect , createSignal , onMount , onCleanup , untrack } from 'solid-js' ;
32import type { AnyRouter } from '@tanstack/solid-router' ;
43import { Color , ShowModalOptions , Utils } from '@nativescript/core' ;
54import { renderPage } from './PageRenderer' ;
@@ -23,6 +22,17 @@ export interface NativeScriptRouterProviderProps {
2322
2423export function NativeScriptRouterProvider ( props : NativeScriptRouterProviderProps ) {
2524 const router = props . router ;
25+
26+ // Ensure router stores are initialized — mirrors RouterContextProvider behavior.
27+ // Without this call, router.stores may be undefined when Transitioner logic
28+ // and useRouterState try to access it.
29+ router . update ( {
30+ ...router . options ,
31+ context : {
32+ ...router . options . context ,
33+ } ,
34+ } ) ;
35+
2636 const log = createDebugLogger ( props . debug ) ;
2737 let frameRef : any ;
2838 let prevIndex = - 1 ;
@@ -63,16 +73,54 @@ export function NativeScriptRouterProvider(props: NativeScriptRouterProviderProp
6373 } , 250 ) ;
6474 } ;
6575
66- // Set up Solid transitions on the router (mirrors Transitioner.tsx)
67- const [ , startSolidTransition ] = useTransition ( ) ;
76+ // ── Simplified state tracking for NativeScript ──
77+ // The standard solid-router uses Transitioner inside a <Suspense> boundary with
78+ // useTransition/startTransition. NativeScript uses Frame-based page navigation
79+ // without Suspense, so we take a simpler approach:
80+ // 1. Execute transitions immediately (no useTransition)
81+ // 2. Use router.subscribe() events + a Solid signal to drive reactive effects
82+ // 3. Manually transition status from pending→idle after load completes
6883 router . startTransition = ( fn : ( ) => void ) => {
69- startTransition ( ( ) => {
70- startSolidTransition ( fn ) ;
71- } ) ;
84+ fn ( ) ;
7285 } ;
7386
74- // Reactive selector: track match IDs to detect route changes
75- const navigationSignal = useStore ( router . __store , ( s : any ) => getNavigationSignalFromRouterState ( s ) ) ;
87+ // Create a Solid signal that updates whenever the router state changes.
88+ // This bypasses the __store reactive chain which depends on Suspense context.
89+ const [ navigationSignal , setNavigationSignal ] = createSignal (
90+ getNavigationSignalFromRouterState ( router . state ) + ':' + ( router . state . location ?. pathname || '' ) ,
91+ ) ;
92+
93+ // Update navigation signal from router state. Includes pathname to detect
94+ // child route changes (e.g., /virtual → /virtual/inspector) where match IDs
95+ // may not update immediately through the __store memo chain.
96+ const updateSignal = ( ) => {
97+ const state = router . state ;
98+ const pathname = state . location ?. pathname || '' ;
99+ const base = getNavigationSignalFromRouterState ( state ) ;
100+ // Append pathname to ensure child route navigations are detected
101+ const next = `${ base } :${ pathname } ` ;
102+ setNavigationSignal ( next ) ;
103+ } ;
104+
105+ // The router emits 'onResolved' when a navigation completes, but we also need
106+ // to detect intermediate states (pending, loading). Use a combination of
107+ // router.subscribe + manual status management.
108+ const unsubResolved = router . subscribe ( 'onResolved' as any , ( ) => {
109+ // Ensure status is set to idle when navigation resolves
110+ batch ( ( ) => {
111+ ( router as any ) . stores . status . setState ( ( ) => 'idle' ) ;
112+ ( router as any ) . stores . resolvedLocation . setState ( ( ) => ( router as any ) . stores . location . state ) ;
113+ } ) ;
114+ updateSignal ( ) ;
115+ } ) ;
116+
117+ const unsubLoad = router . subscribe ( 'onLoad' as any , ( ) => {
118+ updateSignal ( ) ;
119+ } ) ;
120+
121+ const unsubBeforeLoad = router . subscribe ( 'onBeforeRouteMount' as any , ( ) => {
122+ updateSignal ( ) ;
123+ } ) ;
76124
77125 const closeModalFromRouterState = ( ) => {
78126 if ( ! activeModalPage ) {
@@ -398,37 +446,73 @@ export function NativeScriptRouterProvider(props: NativeScriptRouterProviderProp
398446 runNativeBackSync ( ) ;
399447 } ;
400448
449+ // ── Mount lifecycle (mirrors Transitioner.onMount) ──
401450 onMount ( ( ) => {
402- // Subscribe to history changes and trigger router loading (like Transitioner)
403- const unsub = router . history . subscribe ( router . load ) ;
451+ // Subscribe to history changes and update navigation signal after each load.
452+ // The standard Transitioner emits onResolved/onLoad events, but since we
453+ // don't render it, we hook into history changes directly.
454+ const unsub = router . history . subscribe ( ( ) => {
455+ log ( '[NSRouter] history.subscribe fired, pathname:' , router . state . location . pathname ) ;
456+ const loadPromise = router . load ( ) ;
457+ if ( loadPromise && typeof loadPromise . then === 'function' ) {
458+ loadPromise . then ( ( ) => {
459+ // Ensure status transitions to idle after navigation completes
460+ batch ( ( ) => {
461+ ( router as any ) . stores . status . setState ( ( ) => 'idle' ) ;
462+ ( router as any ) . stores . resolvedLocation . setState ( ( ) => ( router as any ) . stores . location . state ) ;
463+ } ) ;
464+ log ( '[NSRouter] history load resolved, updating signal. pathname:' , router . state . location . pathname , 'matches:' , router . state . matches . length ) ;
465+ updateSignal ( ) ;
466+ } ) . catch ( ( err : any ) => {
467+ log ( '[NSRouter] history load error:' , err ?. message || err ) ;
468+ } ) ;
469+ } else {
470+ // router.load() returned synchronously (undefined/void)
471+ log ( '[NSRouter] history load sync, updating signal. pathname:' , router . state . location . pathname ) ;
472+ batch ( ( ) => {
473+ ( router as any ) . stores . status . setState ( ( ) => 'idle' ) ;
474+ ( router as any ) . stores . resolvedLocation . setState ( ( ) => ( router as any ) . stores . location . state ) ;
475+ } ) ;
476+ updateSignal ( ) ;
477+ }
478+ } ) ;
404479
405480 log ( '[NSRouter] onMount: calling router.load()' ) ;
406481 log ( '[NSRouter] latestLocation:' , JSON . stringify ( router . latestLocation ?. pathname ) ) ;
407482 log ( '[NSRouter] routeTree:' , ! ! router . routeTree ) ;
408483 log ( '[NSRouter] routesById keys:' , Object . keys ( ( router as any ) . routesById || { } ) ) ;
409484
410- // Initial load
411- const loadPromise = router . load ( ) ;
412- if ( loadPromise && typeof loadPromise . then === 'function' ) {
413- loadPromise
414- . then ( ( ) => {
415- log ( '[NSRouter] load resolved. status:' , router . state . status , 'matches:' , router . state . matches . length , 'pending:' , router . state . pendingMatches ?. length ) ;
416- } )
417- . catch ( ( err : any ) => {
418- console . error ( '[NSRouter] load rejected:' , err ) ;
419- } ) ;
420- }
421-
422485 // Set up back button handling
423486 const cleanupBack = setupBackHandler ( router , ( ) => frameRef , guard ) ;
424487
425488 onCleanup ( ( ) => {
426489 closeModalFromRouterState ( ) ;
427490 unsub ( ) ;
428491 cleanupBack ( ) ;
492+ unsubResolved ( ) ;
493+ unsubLoad ( ) ;
494+ unsubBeforeLoad ( ) ;
429495 } ) ;
430496 } ) ;
431497
498+ // Initial load — trigger immediately, set idle + update signal when done
499+ {
500+ const tryLoad = async ( ) => {
501+ try {
502+ await router . load ( ) ;
503+ log ( '[NSRouter] load resolved. status:' , router . state . status , 'matches:' , router . state . matches . length , 'pending:' , router . state . pendingMatches ?. length ) ;
504+ batch ( ( ) => {
505+ ( router as any ) . stores . status . setState ( ( ) => 'idle' ) ;
506+ ( router as any ) . stores . resolvedLocation . setState ( ( ) => ( router as any ) . stores . location . state ) ;
507+ } ) ;
508+ updateSignal ( ) ;
509+ } catch ( err : any ) {
510+ console . error ( '[NSRouter] load rejected:' , err ) ;
511+ }
512+ } ;
513+ tryLoad ( ) ;
514+ }
515+
432516 // React to match changes and navigate the Frame
433517 createEffect ( ( ) => {
434518 // Read reactive dependency
@@ -532,11 +616,39 @@ export function NativeScriptRouterProvider(props: NativeScriptRouterProviderProp
532616 }
533617
534618 // Forward or replace navigation
535- const leafMatch = matches [ matches . length - 1 ] ;
619+ let leafMatch = matches [ matches . length - 1 ] ;
536620
621+ // If the leaf match's routeId doesn't match the current pathname, the matches
622+ // may be stale (common for child route swaps within a layout, or parameterized
623+ // routes like /posts/$postId where the leaf shows /posts but pathname is /posts/2).
624+ // Find the correct route by searching all routes for one that matches the pathname.
537625 if ( ! doesRouteIdMatchPathname ( leafMatch . routeId , curPathname ) ) {
538- log ( '[NSRouter] deferring Frame navigation due to pending match/path mismatch:' , 'routeId=' , leafMatch . routeId , 'pathname=' , curPathname , 'status=' , state . status ) ;
539- return ;
626+ const routesById = ( router as any ) . routesById || { } ;
627+ let resolvedRouteId : string | null = null ;
628+
629+ // Search all routes for one whose ID pattern matches the current pathname
630+ for ( const routeId of Object . keys ( routesById ) ) {
631+ if ( routeId === '__root__' ) continue ;
632+ if ( doesRouteIdMatchPathname ( routeId , curPathname ) ) {
633+ // Prefer the most specific match (longest routeId)
634+ if ( ! resolvedRouteId || routeId . length > resolvedRouteId . length ) {
635+ resolvedRouteId = routeId ;
636+ }
637+ }
638+ }
639+
640+ if ( resolvedRouteId ) {
641+ log ( '[NSRouter] match/path mismatch resolved:' , leafMatch . routeId , '->' , resolvedRouteId , 'for pathname:' , curPathname ) ;
642+ leafMatch = { ...leafMatch , routeId : resolvedRouteId } ;
643+ // Schedule a delayed signal update so the match stores settle and
644+ // useParams/useMatch can read correct data when the component renders.
645+ setTimeout ( ( ) => updateSignal ( ) , 16 ) ;
646+ } else {
647+ log ( '[NSRouter] deferring Frame navigation due to pending match/path mismatch:' , 'routeId=' , leafMatch . routeId , 'pathname=' , curPathname , 'status=' , state . status ) ;
648+ // Schedule retry — stores may settle on next tick
649+ setTimeout ( ( ) => updateSignal ( ) , 16 ) ;
650+ return ;
651+ }
540652 }
541653
542654 const route = ( router as any ) . routesById [ leafMatch . routeId ] ;
0 commit comments