NativeScript Blog

TanStack Router Meets NativeScript - A Native Navigation Story

Nathan Walker March 9, 2026

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

If you build framework integrations for NativeScript, there is one mental model that unlocks everything:

  • Frame is the navigator.
  • Page is the screen instance.

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:

  1. Listen to router intent.
  2. Translate that intent into Frame operations.
  3. Render framework UI inside Page instances.
  4. Dispose framework resources when pages leave the native lifecycle.

The Architecture at a Glance

flowchart TD A["TanStack Router<br/>Memory History + Matching +<br/>Loaders"] --> B["NativeScript<br/>Navigation State Mapper"] B --> C["Frame<br/>Native Stack Controller"] C --> D[Page 1] C --> E[Page 2] C --> F[Page N] D --> G["Framework Renderer<br/>SolidJS"] E --> H["Framework Renderer<br/>Reusable Pattern"] F --> I["Framework Renderer<br/>React/Vue/Svelte/Angular<br/>Tomorrow"]

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

sequenceDiagram participant U as User participant L as Link/useNavigate participant R as TanStack Router participant P as NativeScriptRouterProvider participant F as Frame participant G as Page Renderer U->>L: Tap action L->>R: navigate(to) R->>P: state change (matches/location) P->>G: create Page + render route component G-->>P: Page ready P->>F: frame.navigate(page) F-->>U: Native push animation U->>F: Hardware/gesture back F->>P: back navigation event P->>R: history.back() sync R-->>P: previous match state P->>F: frame.goBack() F-->>U: Native pop animation

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:

  1. NativeScript-aware navigation/state core
  2. Framework-specific renderer/context layer
  3. 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 Frame navigation 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.

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.

Join the conversation

Share your feedback or ask follow-up questions below.