TanStack Router Meets NativeScript - A Native Navigation Story
Type safety in navigation brings peace of mind to a rapidly growing codebase among teams of all sizes. It also improves agentic workflows, team understanding and ability to refactor large codebases with greater confidence. That is the cornerstone of TanStack Router, and it's now available for NativeScript apps with @nativescript/tanstack-router.
Built to infer route types deeply including params, search state, and navigation calls, TanStack Router ensures consistency as routes evolve. That means fewer broken links and fewer "stringly"-typed route bugs.
The first implementation introduced below is SolidJS focused, and the architecture is intentionally shaped so other JavaScript frameworks can follow the same path.
This post is part 1 of a 2-part series. Part 1 here covers the native navigation model and the runtime contract. Part 2 covers file-based routing and how TanStack Router's generated route trees fit into the same NativeScript architecture: TanStack Router - Using file routing in NativeScript.
- Frame and Page are the Contract
- The Architecture at a Glance
- How TanStack Router Connects to NativeScript
- End-to-End Navigation Flow
- Why Backstack Pages Require Special Care
- Why SolidJS Was a Strong Target
- Designed for More Than One Framework
- Shared Element Transitions
- What it means for TanStack Router
- 5-Minute Getting Started (Solid + TypeScript)
- Status: Early Developer Preview
- Sample Project
- References
- Help Accelerate the Work
Frame and Page are the Contract
If you build framework integrations for NativeScript, there is one mental model that unlocks everything:
A Frame maintains a real native backstack of Page objects. frame.navigate() pushes. frame.goBack() pops. Those are not simulated route operations. They are native transitions with native lifecycle and native user expectations attached.
This is why NativeScript is such a strong target for JS framework interop. The platform gives you a clear primitive contract:
- Navigation state machine: Frame
- Visual/lifecycle boundary: Page
- Platform behaviors: Android hardware back and iOS swipe-back
For any JavaScript framework, the routing adapter job is then explicit:
- Listen to router intent.
- Translate that intent into Frame operations.
- Render framework UI inside Page instances.
- Dispose framework resources when pages leave the native lifecycle.
The Architecture at a Glance
TanStack Router remains the source of route truth. NativeScript remains the source of navigation runtime truth.
How TanStack Router Connects to NativeScript
TanStack Router provides a mature state layer:
- Type-safe routes
- Params and search parsing
- Loader execution
- Declarative navigation APIs
NativeScript provides a mature runtime navigation layer:
- Native page stack
- Native transition behavior
- Native back affordances
- Page lifecycle events
In this implementation, the provider watches router state and makes directional decisions:
- Forward/replace transitions map to
frame.navigate(...) - Back transitions map to
frame.goBack()
Every leaf match is rendered into its own Page. That keeps route composition explicit and aligned with native stack semantics.
End-to-End Navigation Flow
This is the most important integration quality: no "fake back," no browser emulation layer, no mismatch between what the router believes and what the native stack is doing.
Why Backstack Pages Require Special Care
A subtle but critical detail: NativeScript can keep prior pages alive in the backstack while they are not visible.
That means framework render roots are not always equivalent to active route matches. A hidden page may still be mounted while route matching has moved elsewhere.
This implementation addresses that reality with lifecycle discipline:
- Page render roots are cleaned up on
disposeNativeView. - Hidden backstack pages stay safe until re-activation.
- Error-boundary reset behavior helps page trees recover when returning to foreground after stale-match conditions.
Why SolidJS Was a Strong Target
SolidJS gives this architecture practical advantages:
- Fine-grained reactivity keeps route-state updates efficient.
- Per-page root creation/disposal maps well to NativeScript page lifecycle.
- Error boundary patterns are straightforward for backstack recovery scenarios.
Designed for More Than One Framework
This implementation is a framework-agnostic routing design matching TanStack's principles.
The reusable architecture is:
- NativeScript-aware navigation/state core
- Framework-specific renderer/context layer
- Shared lifecycle and backstack safety rules
Shared Element Transitions
Because NativeScript supports Shared Element Transitions, TanStack Router gets them for free...natively.
The implementation follows a TanStack-aligned pattern:
- Keep navigation intent in
router.navigate(...)options. - Carry per-navigation visual metadata through navigation
state. - Let the NativeScript provider consume that metadata when calling
frame.navigate(...).
This keeps TanStack Router semantics intact while allowing platform-specific visual behavior in NativeScript.
In practice, the source and destination views use a matching sharedTransitionTag, and the Link sets a per-navigation transition:
import { Link, createNativeScriptTransitionState } from '@nativescript/tanstack-router/solid'
import { PageTransition, SharedTransition } from '@nativescript/core'
const avatarTag = `post-author-avatar-${authorId}`
<Link
to={`/posts/${postId}`}
state={createNativeScriptTransitionState(
SharedTransition.custom(new PageTransition(), {
pageEnd: {
sharedTransitionTags: {
[avatarTag]: {
scale: { x: 0.9, y: 0.9 },
},
},
},
}),
)}
>
<image sharedTransitionTag={avatarTag} class="w-[28] h-[28] rounded-full" />
</Link>
// On destination page
<image sharedTransitionTag={avatarTag} class="w-[72] h-[72] rounded-full" />
For teams already using TanStack Router, this should feel familiar: route state remains the extensibility point, while runtime adapters decide how to interpret it.
For larger apps, we recommend extracting this into a small utility so tags and transition config stay centralized:
// src/utils/shared-transitions.ts
import { createNativeScriptTransitionState } from '@nativescript/tanstack-router/solid'
import { PageTransition, SharedTransition } from '@nativescript/core'
export function getPostAuthorAvatarTag(authorId: string): string {
return `post-author-avatar-${authorId}`
}
export function createPostAuthorSharedTransitionState(authorId: string) {
return createNativeScriptTransitionState(
SharedTransition.custom(new PageTransition(), {
pageEnd: {
sharedTransitionTags: {
[getPostAuthorAvatarTag(authorId)]: {
scale: { x: 0.9, y: 0.9 },
},
},
},
}),
)
}
What it means for TanStack Router
If you already use TanStack Router on the web, most of your route-level habits carry over.
The conceptual shift is simply this:
- On web: URL updates drive sections of one app tree.
- On NativeScript: router state drives native page stack operations.
Same routing confidence, different runtime expression.
5-Minute Getting Started (Solid + TypeScript)
If you want to try this quickly, here is the shortest practical path.
1. Create a new NativeScript Solid app
ns create myapp --solid
When prompted for language, choose TypeScript.
Then open the project:
cd myapp
2. Install TanStack Router integration packages
npm install @nativescript/tanstack-router @tanstack/solid-router @tanstack/history
3. Define a tiny route tree
Create src/routes.tsx:
import { createRootRoute, createRoute } from '@nativescript/tanstack-router/solid'
import { Link } from '@nativescript/tanstack-router/solid'
function Home() {
return (
<stackLayout>
<label text="Home" />
<Link to="/about">
<label text="Go to About" />
</Link>
</stackLayout>
)
}
function About() {
return (
<stackLayout>
<label text="About" />
</stackLayout>
)
}
const rootRoute = createRootRoute()
const homeRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: Home,
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
component: About,
})
export const routeTree = rootRoute.addChildren([homeRoute, aboutRoute])
4. Create router and provider
Update src/app.tsx:
import {
NativeScriptRouterProvider,
createNativeScriptRouter,
} from '@nativescript/tanstack-router/solid'
import { routeTree } from './routes'
const router = createNativeScriptRouter({
routeTree,
initialPath: '/',
})
declare module '@nativescript/tanstack-router/solid' {
interface Register {
router: typeof router
}
}
export function App() {
return (
<gridlayout>
<NativeScriptRouterProvider
router={router}
debug={true}
actionBarVisibility="always"
/>
</gridlayout>
)
}
5. Run and verify navigation
ns run ios
# or
ns run android
Tap Go to About and verify a native push transition occurs.
At this point, you have the essential adapter loop running:
- TanStack Router state changes
- NativeScript
Framenavigation actions - Route components rendered per native
Page
From here, you can layer in typed params, loaders, and Link back behavior.
Status: Early Developer Preview
This is an Early Developer Preview.
It is for experimentation, feedback, and collaboration.
It is not intended for production use at this stage.
If the direction is useful for your team, now is the best time to explore and influence what a production-ready cross-framework version should become.
Sample Project
The sample project for this implementation can be found here.
References
Help Accelerate the Work
If you want to help move this effort forward, sponsorship directly increases the time and focus available for implementation quality, test coverage, and framework expansion.
- Open Collective: https://opencollective.com/nativescript
- GitHub Sponsors: https://github.com/sponsors/NativeScript
If enough teams rally around this, TanStack-style routing with first-class native stack behavior can become a durable foundation for all JavaScript apps on native platforms.