Skip to content

Commit 5093a18

Browse files
committed
fix: compat with latest 1.168+ tanstack
1 parent b002eaf commit 5093a18

File tree

1 file changed

+138
-26
lines changed

1 file changed

+138
-26
lines changed

packages/tanstack-router/src/NativeScriptRouterProvider.tsx

Lines changed: 138 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
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';
32
import type { AnyRouter } from '@tanstack/solid-router';
43
import { Color, ShowModalOptions, Utils } from '@nativescript/core';
54
import { renderPage } from './PageRenderer';
@@ -23,6 +22,17 @@ export interface NativeScriptRouterProviderProps {
2322

2423
export 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

Comments
 (0)