NativeScript Blog

TanStack Router Meets NativeScript - A Native Navigation Story

Nathan Walker March 9, 2026

Imagine type-safe navigation across your whole app no matter the framework or target platform. You can have it today with TanStack Router, which is built to infer route types deeply, so params, search state, and navigation calls stay consistent as routes evolve. That means fewer broken links, fewer stringly-typed route bugs, and better refactors in large NativeScript apps.

There is also a nice benefit around URL-like state even on native. TanStack Router treats search params as a first-class state layer. On mobile, that does not have to mean browser URLs only. It can mean "serializable, shareable, debuggable navigation state" for filters, tabs, pagination, sort, selected item IDs, and modal state. That is useful for deep links, app restoration, analytics, and reproducible bug reports. Plus you can have all that while also having truly native iOS and Android navigation, including stack transitions, hardware back, and gesture-driven back.

@nativescript/tanstack-router achieves this by keeping navigation native to the target platform, while using TanStack Router as the typed, predictable control plane. The first implementation is SolidJS focused, and the architecture is intentionally shaped so other JavaScript frameworks can follow the same path.


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\nMemory History + Matching + Loaders] --> B[NativeScript\nNavigation State Mapper] B --> C[Frame\nNative Stack Controller] C --> D[Page 1] C --> E[Page 2] C --> F[Page N] D --> G[Framework Renderer\nSolidJS] E --> H[Framework Renderer\nReusable Pattern] F --> I[Framework Renderer\nReact/Vue/Svelte/Angular Tomorrow]

TanStack Router remains the source of route truth. NativeScript remains the source of navigation runtime truth.

The implementation does the translation between those two truths.

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 here.

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 <NativeScriptRouterProvider router={router} actionBarVisibility="always" />
}

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 JavaScript apps on mobile.

Join the conversation

Share your feedback or ask follow-up questions below.