TanStack Router - Using file routing in NativeScript
In the first post, we focused on the navigation contract itself: why TanStack Router works so naturally with NativeScript once you accept that Frame and Page are the runtime truth.
That post focused on the runtime contract:
- how TanStack Router drives a real native page stack
This post focuses on the authoring model:
- how file-based routing fits into that same architecture without changing the native navigation story
TanStack Router's file routing gives NativeScript apps a route authoring model that is easier to scale, easier to read, easier to refactor, and still fully type-safe. More importantly, it does that without changing the native runtime contract at all.
The router still decides matches, params, search state, and loaders.
NativeScript still decides page creation, transitions, backstack behavior, and lifecycle.
File routing simply improves how the route tree is declared.
- Why File Routing Matters More on Native Than You Might Expect
- The Core Idea: Generate, Then Run Natively
- What Makes the Vite Integration Work
- The Minimum App Structure
- The File Route Config
- Physical File Routes in Practice
- Why This Works So Well with NativeScript Pages
- File Routing Still Preserves Native Modal Behavior
- Generated Types Are the Quiet Superpower
- Virtual Routes: The Other Half of the Story
- The Virtual Route
- Why Virtual Routes Are a Natural Fit for Native Apps
- The Virtual Screen Files Are Still Ordinary Route Modules
- Why the Pairing Feels So Cohesive
- What About Code-Based Routes?
- A Practical Setup Path
- An Important Implementation Note
- Sample App
- References
- Help Accelerate the Work
Why File Routing Matters More on Native Than You Might Expect
Native apps grow route trees quickly:
- list screens
- detail screens
- modal screens
- settings areas
- nested flows
- deep links
- auth boundaries
With code-based route declarations, those relationships stay precise, but the route definition layer gets longer and denser over time.
File routing shifts that complexity into the filesystem, where teams already expect to reason about screen structure.
That gives you a practical scaling model:
- route path shape becomes visible from the directory layout
- params become visible from route file names
- route-local loader logic stays near the screen
- page-level refactors become easier because route ownership is more obvious
And because this is TanStack Router, you are not giving up type inference to get that convenience.
You still keep strongly typed:
Linkdestinations- route params
- search state
- loader data
- route APIs
That combination is exactly why this pairing works so well.
The Core Idea: Generate, Then Run Natively
The working model is simple:
- TanStack Router reads route files.
- It generates
routeTree.gen.ts. - The app creates a router from that generated tree.
NativeScriptRouterProviderturns router state into native navigation.
That means file routing lives entirely on the authoring and generation side.
Runtime code stays lean:
import {
NativeScriptRouterProvider,
createNativeScriptRouter,
} from '@nativescript/tanstack-router/solid'
import { routeTree } from './routeTree.gen'
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} />
</gridlayout>
)
}
That is important. The file-route system does not complicate the NativeScript side. It keeps the router creation story clean.
What Makes the Vite Integration Work
@nativescript/vite already has an extensibility model where installed packages can contribute additional Vite configurations through an external nativescript.vite.mjs file.
That means @nativescript/vite can discover configurations from installed dependencies without forcing every app to wire custom plugin code manually.
So the file-routing story works like this:
- The app installs
@nativescript/tanstack-router. @nativescript/vitediscovers@nativescript/tanstack-router/nativescript.vite.mjs.- That config enables the TanStack Router Vite plugin when the app has file-route signals such as
tsr.config.jsonorsrc/routes. - TanStack generates
routeTree.gen.tsautomatically.
That is a particularly strong fit for NativeScript because it keeps the app-level Vite config extremely small:
import { defineConfig, UserConfig } from 'vite'
import { solidConfig } from '@nativescript/vite/solid'
export default defineConfig(({ mode }): UserConfig => {
return solidConfig({ mode })
})
The app does not need to manually compose the TanStack plugin in its own Vite file.
The Minimum App Structure
From our app, the main pieces are:
src/
app.tsx
routeTree.gen.ts
routes/
__root.tsx
index.tsx
about.tsx
posts.tsx
posts.$postId.tsx
users.$userId.tsx
-virtual/
layout.tsx
index.tsx
inspector.tsx
tsr.config.json
The generated file is exactly what you would expect:
- it imports each route module
- assigns parent relationships
- builds the route tree
- augments TanStack's route types
That generated augmentation is the reason typed links like to="/posts/$postId" and params={{ postId: '2' }} continue to work.
The File Route Config
For the physical route portion, the sample uses a standard tsr.config.json:
{
"routesDirectory": "./src/routes",
"generatedRouteTree": "./src/routeTree.gen.ts",
"routeFileIgnorePrefix": "-",
"quoteStyle": "double",
"semicolons": true
}
That tells TanStack where route files live, where to generate the route tree, and that files prefixed with - should not be discovered as ordinary physical routes.
That last point becomes especially useful for virtual route patterns.
Physical File Routes in Practice
Root route:
import { createRootRoute } from '@tanstack/solid-router'
export const Route = createRootRoute()
Index route:
import Home from '../components/home'
import { getHomeLoaderData } from '../demo-data'
export const Route = createFileRoute({
component: Home,
loader: async () => getHomeLoaderData(),
})
Dynamic param route:
import PostDetail from '../components/post-detail'
import { getPostDetailLoaderData } from '../demo-data'
export const Route = createFileRoute({
component: PostDetail,
loader: async ({ params }) => getPostDetailLoaderData(params.postId),
})
A few things matter here:
- loaders stay route-local
- route params stay typed
- route modules stay small and focused
- the runtime does not care whether the route tree came from files or code
Why This Works So Well with NativeScript Pages
The fit is strong because NativeScript already thinks in screens.
A route file in this setup is not merely a URL segment definition. It maps naturally to a screen boundary, or at least to a page-level screen concern.
That leads to a very clean mental model:
- route file defines route behavior
- screen component defines page UI
- loader defines data contract
- generated route tree defines type-safe connectivity
- NativeScript provider defines native navigation behavior
Those responsibilities remain separated, which is exactly why the system stays understandable as the app grows.
File Routing Still Preserves Native Modal Behavior
In the app, modal behavior is still expressed through TanStack Router state and search handling, while the NativeScript provider interprets that into a real modal presentation.
That means file routing and native navigation specialization remain complementary:
- route files define the route
- typed search state triggers modal intent
NativeScriptRouterProviderdecides how that intent is rendered natively
Generated Types Are the Quiet Superpower
The generated routeTree.gen.ts file is not just busywork. It is what turns route files into a strongly typed control plane.
In the current sample, the generated tree contains entries such as:
//about/posts/posts/$postId/users/$userId/virtual/virtual/inspector
It also augments the router with information like:
FileRoutesByFullPathFileRoutesByToFileRoutesById
That is why links become self-checking:
<Link to="/posts/$postId" params={{ postId: '2' }}>
<label text="Open" />
</Link>
If the route path changes, or params no longer match, TypeScript tells you.
That is exactly the kind of feedback loop that keeps navigation refactors safe.
Virtual Routes: The Other Half of the Story
Physical file routes are only half of what makes TanStack Router's file story interesting.
The other half is virtual routing.
Virtual routes matter when you want route structure to come from configuration rather than raw directory shape.
That is useful when:
- you want to group screens differently than folders are laid out
- you want a subtree declared centrally
- you want to mix physical discovery with explicit mounted route files
- you want to keep certain route files hidden from normal file discovery
We added a /virtual subtree to demonstrate exactly that in the app.
The Virtual Route
The app currently uses an inline virtualRouteConfig inside tsr.config.json:
{
"routesDirectory": "./src/routes",
"generatedRouteTree": "./src/routeTree.gen.ts",
"virtualRouteConfig": {
"type": "root",
"file": "__root.tsx",
"children": [
{
"type": "physical",
"directory": ".",
"pathPrefix": ""
},
{
"type": "route",
"path": "virtual",
"file": "-virtual/layout.tsx",
"children": [
{
"type": "index",
"file": "-virtual/index.tsx"
},
{
"type": "route",
"path": "inspector",
"file": "-virtual/inspector.tsx"
}
]
}
]
},
"routeFileIgnorePrefix": "-"
}
That config does two things at once:
physical('.')behavior is represented by the physical subtree entry, preserving the ordinary file-routed app.- A virtual
/virtualsubtree is mounted from files that are intentionally ignored by normal discovery.
That second part is important.
The files under src/routes/-virtual are not treated as ordinary physical route files. They exist specifically so the virtual config can mount them where it wants.
That is a powerful pattern for larger apps.
Why Virtual Routes Are a Natural Fit for Native Apps
Native apps often have flows that are conceptually grouped, but not necessarily best expressed by raw directory structure alone.
For example:
- onboarding flows
- settings sections
- account areas
- feature-flagged route clusters
- multi-screen transactional flows
Virtual routing lets you express those relationships intentionally.
That is especially useful in NativeScript because the route tree is not just about URLs. It also becomes a very readable definition of screen hierarchy and page intent.
The Virtual Screen Files Are Still Ordinary Route Modules
Even though /virtual is mounted from config, the screen files themselves still look like standard TanStack file route modules.
Example layout route:
import { Outlet } from '@tanstack/solid-router'
import { Link } from '@nativescript/tanstack-router/solid'
export const Route = createFileRoute({
component: VirtualRoutesLayout,
})
function VirtualRoutesLayout() {
return (
<>
<actionbar title="Virtual Routes" />
<scrollview>
<stacklayout>
<Link to="/virtual">
<label text="Overview" />
</Link>
<Link to="/virtual/inspector">
<label text="Inspector" />
</Link>
<Outlet />
</stacklayout>
</scrollview>
</>
)
}
That is another reason this pairing works well.
Virtual routing changes placement and mounting, not the component authoring model.
Why the Pairing Feels So Cohesive
There are several reasons why this may feel better than many native/web hybrid routing stories.
1. The runtime responsibilities are not confused
TanStack Router handles:
- route matching
- parsing
- params
- loaders
- typed navigation intent
NativeScript handles:
- page stack mutations
- modal presentation
- transitions
- back button behavior
- lifecycle boundaries
Those are clean boundaries.
2. File routing improves authoring without changing execution
That is exactly what you want from a routing authoring feature.
3. Type generation keeps the developer experience honest
This is not a loose convention system. The generated route tree becomes a compile-time contract.
4. Screen-oriented apps map naturally to route files
NativeScript already encourages screen-level thinking. File routes reinforce that instead of fighting it.
What About Code-Based Routes?
They still work.
File routing is additive, not replacement-only.
Teams that prefer explicit code-based route trees can keep them.
Teams that want filesystem-driven authoring can use file routes.
Teams that want a hybrid story can use physical file routes plus virtual route configuration for selected areas.
A Practical Setup Path
If you want to try file routing in a NativeScript Solid app today, the shortest path looks like this.
1. Create the app
ns create myapp --solid
cd myapp
2. Install the router packages
npm install @nativescript/tanstack-router @tanstack/solid-router @tanstack/history
# devDependency only
npm install -D @nativescript/vite
Init Vite:
npx nativescript-vite init
If you want to author virtual route config directly, also install:
npm install -D @tanstack/virtual-file-routes
3. Add route files
Create:
src/routes/__root.tsxsrc/routes/index.tsxsrc/routes/about.tsx
4. Add tsr.config.json
{
"routesDirectory": "./src/routes",
"generatedRouteTree": "./src/routeTree.gen.ts",
"routeFileIgnorePrefix": "-",
"quoteStyle": "double",
"semicolons": true
}
5. Create the NativeScript router from the generated tree
import {
NativeScriptRouterProvider,
createNativeScriptRouter,
} from '@nativescript/tanstack-router/solid'
import { routeTree } from './routeTree.gen'
6. Run the app
ns run ios
# or
ns run android
At that point, the route tree is generated and your navigation remains fully native.
An Important Implementation Note
In the app, the virtual routing example is shown using the inline virtualRouteConfig object inside tsr.config.json.
That detail is intentional.
It reflects the version of the generator currently being exercised in the NativeScript sample and keeps the example aligned with what is working cleanly end-to-end right now.
As this ecosystem evolves, additional config-authoring styles such as external virtual route config files and subtree config helpers can be expanded further. But the key integration idea is already proven:
- physical file routes work
- mixed virtual plus physical routing works
- NativeScript runtime behavior remains native
Sample App
The working sample app used for this post can be found here:
References
- Part 1: TanStack Router Meets NativeScript
- TanStack Router Documentation
- NativeScript Vite Documentation
- NativeScript Frame Documentation
- NativeScript Page Documentation
- SolidJS
- NativeScript TanStack Router Repository
Help Accelerate the Work
This is still early work, and there is plenty of room to improve the ergonomics, examples, tests, and cross-framework reach.
If this direction is useful to your team, support helps turn these ideas into durable tooling.
- Open Collective: https://opencollective.com/nativescript
- GitHub Sponsors: https://github.com/sponsors/NativeScript