Hello! Welcome back to our course on migrating your SPA to TanStack Start.
In our last lesson, we focused on a critical aspect of SSR: managing the execution boundary. You learned how to isolate browser-specific APIs like window and document using tools like <ClientOnly>, createIsomorphicFn, and the ssr: false route option. This ensures your application can render on the server without crashing.
Now, we'll shift our focus from the component level to the application's very structure. The learning outcome for this lesson is to refactor an existing client-side router to use TanStack's file-based routing conventions.
For a developer with your experience, you're likely very familiar with programmatic routers like react-router-dom, where you define your application's routes explicitly in code. TanStack Router, which powers TanStack Start, uses a different, convention-based paradigm. Migrating to its file-based system is a foundational step that unlocks many of the framework's most powerful features, including type-safe navigation, automatic code-splitting, and streamlined server-side data fetching.
1. The Shift in Paradigm: Programmatic vs. File-Based Routing
In a typical client-side React application, you might define your routes like this:
// Example using react-router-dom
import { Routes, Route } from 'react-router-dom';
import { AppLayout, HomePage, AboutPage, PostsLayout, PostsIndexPage, PostPage } from './components';
function App() {
return (
<Routes>
<Route path="/" element={<AppLayout />}>
<Route index element={<HomePage />} />
<Route path="about" element={<AboutPage />} />
<Route path="posts" element={<PostsLayout />}>
<Route index element={<PostsIndexPage />} />
<Route path=":postId" element={<PostPage />} />
</Route>
</Route>
</Routes>
);
}
This is programmatic routing. You explicitly declare the relationship between paths and components in your JSX.
TanStack Router champions file-based routing, where the file and folder structure inside a special src/routes directory directly defines your application's routes. This approach offers several advantages that are particularly beneficial in a full-stack framework.
File-Based Routing | TanStack Router React Docs
First, let's read the official documentation to understand the core benefits of this approach.
Read the section "What is File-Based Routing?". Pay attention to the benefits listed, especially code-splitting and type-safety, which are major motivators for this migration.
To see this in action, the following video provides an excellent walkthrough of setting up a project with file-based routing from scratch. It clearly demonstrates how creating files translates directly into navigable routes.
Tanstack Router in React (Complete Tutorial)
This tutorial from Cosden Solutions is a great practical introduction to TanStack's routing conventions.
Watch from the beginning until 11:41. Focus on how the creation of files inside the src/routes directory automatically generates a type-safe routeTree.gen.ts file and makes those routes available to the application.
2. Mapping Your Routes: From Code to Files
The central task of this refactor is to translate your programmatic route definitions into a corresponding file structure. Let's use our react-router-dom example from above and map it to TanStack's conventions.
Here are the key conventions you'll use:
| Convention | Description | react-router-dom Equivalent | TanStack File Structure |
|---|---|---|---|
| Root Layout | The main layout for the entire app. | <Route path="/" element={<AppLayout />}> | src/routes/__root.tsx |
| Index Route | The component for the root path (/). | <Route index element={<HomePage />} /> | src/routes/index.tsx |
| Simple Route | A component for a specific path. | <Route path="about" element={<AboutPage />} /> | src/routes/about.tsx |
| Layout Route | A file that provides a shared layout for a group of routes. | <Route path="posts" element={<PostsLayout />}> | src/routes/posts.tsx |
| Nested Index | The default component for a nested path. | <Route index element={<PostsIndexPage />} /> | src/routes/posts/index.tsx |
| Dynamic Route | A route with a URL parameter. | <Route path=":postId" element={<PostPage />} /> | src/routes/posts/$postId.tsx |
The official documentation provides clear tables illustrating these patterns, including "flat" routing (e.g., posts.$postId.tsx) and directory-based routing. For a migration, starting with the more explicit directory structure is often clearer.
File-Based Routing | TanStack Router React Docs
For a detailed reference on these conventions, consult the official documentation.
Review the tables in the sections "Directory Routes", "Flat Routes", and "Mixed Flat and Directory Routes". You don't need to memorize them, but familiarize yourself with how file and folder names map to URL paths.
3. The Incremental Migration Strategy
As a lead developer, you know that a "big bang" refactor is risky. Fortunately, you can migrate your routing incrementally. The key is to run both routing systems in parallel during the transition, moving one route at a time.
This video, while focused on a Next.js to TanStack migration, outlines an excellent general strategy for this kind of work.
How I Migrated Next.js to React Router and TanStack
This clip from Alem Tuzlak's migration video discusses the practicalities of a route-by-route migration.
Watch from 15:20 to 19:11. The key idea is to handle the migration incrementally, route by route, rather than attempting to move the entire application at once. This minimizes risk and allows for continuous delivery.
Here is a step-by-step plan for your migration:
Step 1: Initial Setup
First, install the necessary packages and configure the Vite plugin. This plugin is what inspects your src/routes directory and generates the routeTree.gen.ts file.
Manual Setup | TanStack Router React Docs
The manual setup guide in the TanStack Router docs provides the exact code needed for this.
Follow the steps under "Using File-Based Route Generation". You'll need to install the packages, configure vite.config.ts, and update your main.tsx to use the new RouterProvider.
Step 2: Create the Root Layout (__root.tsx)
This is the entry point for all your TanStack routes.
- Create
src/routes/__root.tsx. - Move your main application layout component (e.g.,
AppLayoutcontaining navbars, footers) into this file. - Crucially, place the
<Outlet />component from@tanstack/react-routerwhere you want the child routes to be rendered.
// src/routes/__root.tsx
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
export const Route = createRootRoute({
component: () => (
<>
<header>
<nav>
{/* We will replace these links next */}
<Link to="/" className="[&.active]:font-bold">Home</Link>
<Link to="/about" className="[&.active]:font-bold">About</Link>
<Link to="/posts" className="[&.active]:font-bold">Posts</Link>
</nav>
</header>
<main>
{/* Child routes will be rendered here */}
<Outlet />
</main>
<TanStackRouterDevtools />
</>
),
})
Step 3: Replace Navigation Components
You need to replace all instances of your old router's navigation components with TanStack's equivalents.
- Replace
<Link from="react-router-dom">with<Link from="@tanstack/react-router">. The props are very similar, buthrefis nowto. - Replace
useNavigate from "react-router-dom"withuseNavigate from "@tanstack/react-router".
The type-safety of TanStack Router shines here. The to prop on <Link> will be typed based on the routes you've created in your file system!
Step 4: Migrate Routes One by One
Start with a simple route, like /about.
- Create
src/routes/about.tsx. - Move the content of your
AboutPagecomponent into it. - Export the route definition using
createFileRoute.
// src/routes/about.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/about')({
component: AboutComponent,
})
function AboutComponent() {
return <div>This is the about page.</div>
}
Once this works, you can remove the /about route from your old react-router-dom configuration. Repeat this process for all your routes.
4. Handling Dynamic Routes and Parameters
Migrating dynamic routes requires one extra step: accessing the URL parameters.
With react-router-dom, you use the useParams hook:const { postId } = useParams();
With TanStack Router, you also use a useParams hook, but it's imported from @tanstack/react-router and is fully type-safe.
This video segment demonstrates creating a dynamic route and accessing its parameters.
Tanstack Router in React (Complete Tutorial)
Let's return to the Cosden Solutions tutorial to see a clear example of creating and using dynamic routes.
Watch from 16:21 to 21:01. This part shows how to create a dynamic route file ($postId.tsx), link to it with parameters, and then access those parameters inside the component.
Here's a before-and-after for a dynamic post page:
Before: react-router-dom
// src/components/PostPage.jsx
import { useParams } from 'react-router-dom';
export function PostPage() {
const { postId } = useParams();
// ... fetch post data using postId
return <h1>Post: {postId}</h1>;
}
After: TanStack Router
// src/routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router';
// The loader is the ideal place to fetch data, as we covered previously.
// The `params` are automatically passed to the loader.
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
// const post = await fetchPostById(params.postId);
// return { post };
return { postId: params.postId }; // For demonstration
},
component: PostPageComponent,
});
function PostPageComponent() {
// You can get params from the loader's data or directly with the hook
const { postId } = Route.useLoaderData();
// OR: const params = Route.useParams();
return <h1>Post: {postId}</h1>;
}
Notice how this structure naturally encourages moving data-fetching logic into the loader, connecting directly to what we learned in a previous lesson.
Conclusion
Today we've mapped out a clear, strategic path for refactoring your application's routing from a programmatic, client-side model to TanStack's powerful, convention-based file system.
Key Takeaways:
- Paradigm Shift: You're moving from explicitly defining routes in code (
<Route>) to letting the file system insrc/routesdefine them by convention. - Core Conventions: The
__root.tsxfile establishes the global layout with an<Outlet />,index.tsxfiles serve as path defaults, and the$prefix (e.g.,$postId.tsx) creates dynamic routes. - Incremental Migration: The most robust approach is to set up the new router in parallel and migrate routes one by one, replacing navigation components like
<Link>anduseNavigateas you go. - Type-Safety: A major benefit of this migration is that TanStack Router generates types for all your routes, ensuring you can't link to a non-existent page or forget a required parameter.
In our next lesson, we will build upon this new route structure. We will implement a progressively enhanced form using TanStack Start server functions for submission and validation, a pattern that leverages the tight integration between routing and server-side logic.
Can't find a good explanation? Sign up and we'll make it for you
Sign up