Telerik blogs

See how to set up a TanStack Router project and define routes using the file-based approach, handle dynamic routes, loaders, error boundaries and even protected routes.

Since the introduction of single-page applications, the quest for a more effective approach to handling core operations, such as routing, has increased. Although we have a couple of other React-supported client-side libraries available, including React Router, modern React frameworks such as Remix and Next.js come with their own built-in routing mechanisms. But why settle if you can get more?

TanStack Router, created by the TanStack team (same team behind React Query), introduces a fresh take on routing that offers improvements to the underlying routing mechanism. With features such as first-class TypeScript support, built-in data fetching with caching, full-fledged search params API with validation and more, it empowers developers to build modern and scalable applications with predictable behavior and fewer edge cases. Unlike traditional routing solutions, TanStack Router gives you complete control over your route definitions.

This article provides a head start on what TanStack Router is, highlighting its benefits and providing a guide on how to get up and running with it.

Prerequisites

To follow along with this article, you should have a basic understanding of React. No prior experience with routing libraries is required.

What Is TanStack Router?

TanStack Router is a client-side routing library that does way more than what a typical traditional routing solution offers. It addresses the gray areas common among external routing libraries and even some of the built-in ones that can be found in frameworks such as Next.js and Remix.

As an application scales, developers sometimes encounter limitations in the context of routing. Type safety is usually not a core focus in many of these other routers, so there’s no helpful feedback or auto-correction when you mistype a route or use the wrong parameters. Also, data fetching is often treated as a separate concern, which means you have to write extra hooks or put other things in place manually.

With traditional routing solutions, working with search parameters can also be tedious, since parsing and validating them is left entirely up to the developer. On top of that, creating nested layouts or sharing state between routes can get messy as well, especially when the routing system is tied to a strict file structure that doesn’t adapt well to more complex UI needs.

This is where TanStack Router comes in. It is built from the ground up to address these exact limitations, and it offers a developer-first solution that scales efficiently.

Features of Tanstack Router

TanStack Router provides all the core routing capabilities expected from a modern client-side router. Some of these include:

  • Nested Routes – Deeply structured route hierarchies with shared layouts and context
  • Layout Routes – Layouts that wrap groups of routes
  • Grouped Routes – Logical route organization without affecting the URL path structure
  • File-based Routing – A file system approach to routing
  • Parallel Data Loading – Data fetching for multiple routes in parallel to speed up rendering
  • Prefetching – Loading of route data and components ahead of time
  • Error Boundaries and Handling – Route-based error boundaries and fallbacks
  • SSR (Server-Side Rendering) – Rendering of pages on the server with support for streaming
  • Route Masking – Customization and mapping of internal route structure to different URL paths

In addition to these, TanStack Router offers some more powerful, developer-focused improvements that make routing more scalable and maintainable. Here are some of them:

Typesafe Routing

If you’ve been around the frontend ecosystem for a while, you should know that TypeScript has now become a big deal. TanStack Router is built with TypeScript in mind. From route paths to params and search queries, everything is type-safe by default. This means fewer bugs and more help from your editor as you build. This also means that we can navigate to any route in our application with type safety and assurance that the link or navigation call will succeed.

Full Search Param API

Search parameters are often referred to as a first-class citizen in TanStack Router. Based on the traditional web URLSearchParams, TanStack Router uses a robust parser/serializer to handle complex, deeply nested data in search parameters, while keeping everything type-safe and developer-friendly. It also provides a means to validate the parameters. The TanStack team, in their official documentation, refers to it as having a useState right in the URL. You can see how powerful that is.

Full Developer Control

Regardless of whatever routing feature you want to implement, whether it’s matching logic, blocking navigations or route behavior customization, TanStack Router gives low-level access to make advanced routing decisions without hacking around the system. It also provides dedicated devtools that help visualize all of the inner workings of TanStack Router, which makes development easier.

Built-in Data Loading

Each route in a TanStack Router-enabled application can define its own loader function for fetching data. Loaders run before the route renders, making it easy to fetch the needed data beforehand. It also has built-in support for caching and background invalidation.

Route Context Inheritance

TanStack Router enables the definition of route-specific context that automatically flows down to all child or nested routes. This context can hold shared data or configurations, which are loaded either synchronously or asynchronously. It comes in handy in use cases such as authentication, theming or passing global helpers.

How TanStack Router Compares to Other Routers

Here is a table showing how TanStack Router compares with other popular routers.

How TanStack router compares to other routers

Based on the table above, we can see that TanStack Router stands out among others for its deep TypeScript integration, powerful search parameter handling and flexible route context.

While frameworks like Next.js and Remix offer tight routing conventions, TanStack Router gives more control and framework-agnostic flexibility, making it ideal for teams building custom and large-scale React applications.

Setting Up a Project with Tanstack Router

There are two main approaches to getting started with a TanStack Router project. First, the TanStack team provides a create-tsrouter-app project starter, which goes through the steps to create a new React project with TanStack Router already configured.

Run the command below to get that up and running:

npx create-tsrouter-app@latest my-app --template file-router

A second approach is to do the setup manually. This is the approach you’ll need to use if you are integrating TanStack Router into an existing application.

In your preferred directory, run the command below:

npm create vite@latest tanstack-router-demo -- --template react-ts

This creates a new React project with the name tanstack-router-demo. You can change this name to whatever you like. The project also uses the react-ts template that enables TypeScript.

Change the directory to the newly created project’s folder:

cd tanstack-router-demo

Then run this command to install the necessary dependencies:

npm install

Next, run the following command to install TanStack Router and also the devtools it provides:

npm install @tanstack/react-router @tanstack/react-router-devtools

We also need to install the TanStack Router Vite plugin as a development dependency:

npm install -D @tanstack/router-plugin

It is mandatory to add the plugin to the project’s Vite config. Open the vite.config.ts file and update the code as shown below:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    TanStackRouterVite({ target: "react", autoCodeSplitting: true }),
    react(),
  ],
});

Note that the plugin has to be added before the React plugin.

Defining Routes

Now that we’ve installed TanStack Router and its necessary plugins, it’s time to define our routes. TanStack Router gives us two ways for route definition: code-based routing and file-based routing.

With code-based routing, route trees are manually created using functions like createRoute and createRootRoute, imported from TanStack Router. This enables the creation of routes programmatically.

On the other hand, TanStack Router also provides a powerful file-based routing experience. Route trees are automatically generated based on a specific file structure. This makes route setup easy, especially for developers familiar with frameworks like Next.js or Remix.

For this article, we’ll be using file-based routing to define and organize our routes.

Creating the Root Layout

In TanStack Router, the root layout is the first route that must be defined. It is the top-most route and it wraps around all other nested routes as children. You can think of it as the foundation of the routing setup.

TanStack Router provides a createRootRoute() function that we can use to create the root layout. Add a routes folder to the src/ folder. This will be where all the application’s route folders and files will be defined.

Now, to include the root layout, create a __root.tsx file in the src/routes/ folder and add the following to it:

import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";

export const Route = createRootRoute({
  component: () => (
    <>
      <div className="p-2 flex gap-2">
        <Link to="/">Home</Link> <Link to="/books">Books</Link>
      </div>
      <hr />
      <Outlet />
      <TanStackRouterDevtools />
    </>
  ),
});

The file has to be explicitly named __root.tsx (with double underscores).

Let’s understand what is going on in this file. We defined the root layout to render two links that point to a Home and Books page. The file uses the following components:

  • createRootRoute: A function used to define the root route of the application; it’s a special type of route that serves as the base for all others
  • Link: A component provided by TanStack Router for navigating between routes
  • Outlet: Where child routes are rendered, it acts as a placeholder for the route that is currently active under the root; you can think of it as a slot
  • TanStackRouterDevtools: Adds a helpful set of developer tools

Configuring the TanStack Router

Once we’ve created the root layout, the next step is to configure the router instance and set it up to work with our React application.

Head over to the main.tsx file in the src/ directory and add the following to it:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import { createRouter, RouterProvider } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

const router = createRouter({ routeTree })

declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <RouterProvider router={router} />
  </StrictMode>,
)

In the code above, we created an instance of the router based on the route tree that will be generated automatically by the TanStack Router plugin from the application’s file structure. You don’t need to worry about what the content of the file will be for now, because we will work on that in the next section.

We then pass this router instance into the RouterProvider component, also from TanStack Router, which makes it available throughout the application.

Take note of how the provider component replaces the predefined <App /> component.

Don’t worry if your code editor is flagging type or import errors related to routes or the route tree at this stage. Everything will fall into place as we move on.

Creating Custom Routes

With the root layout and router configuration done, let’s add some custom pages to our application.

When working with file-based routing in TanStack Router, the file structure inside the src/routes folder maps directly to the route paths in the application. This means the names of the route files (and folders) determine the URLs that can be visited. The concept of index files also applies here.

For example:

  • The file src/routes/index.tsx maps to the Home page of the application
  • The file src/routes/books/index.tsx maps to “/books”

Now, unlike the root layout that uses the createRootRoute function, regular custom routes in a file-based routing setup with Tanstack Router use a createFileRoute function. There are other functions for route creation for other use cases.

CreateFileRoute matches exactly the path passed and also renders the assigned component for the page.

To create the Home page of our application, create an index.tsx file in the src/routes folder and add the following to it:

import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/")({
  component: Index,
});

function Index() {
  return (
    <div>
      <h1>Home Page</h1>
    </div>
  );
}

Here, we imported and instantiated the createFileRoute instance and passed "/" as the path. Then we configured this instance to render a basic Index component when the "/" path is matched.

Now, let’s add a separate "/books" route. Create a src/routes/books/index.tsx file and add the following:

import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/books")({
  component: Books,
});

function Books() {
  return (
    <div>
      <h2>Book List</h2>
      <ul>
        <li>Atomic Habits</li>
        <li>Deep Work</li>
        <li>A Random Walk Down Wall Street</li>
      </ul>
    </div>
  );
}

Here, we also configured the createFileRoute and defined a list of books to be rendered when the "/books" path is matched.

Ignore the type errors and save the files. In the next section, when we look at route trees, we’ll see how all these align.

Route Trees

The route tree is a hierarchical representation of all the routes in an application. It defines the structure of the routes and their relationship with one another. When using the file-based routing setup with TanStack Router, this route tree is usually automatically built using the folder and file structure inside the src/routes folder.

So far in our demo application, we’ve created a src/routes/__root.tsx file for the root layout and two other custom route files. These files act like nodes on the route tree and are automatically configured.

Let’s run the development server and see what happens under the hood.

npm run dev

When we run this, the TanStack Router Vite plugin comes in. It scans the src/routes folder, maps the structure of the route files, and automatically generates a file called routeTree.gen.ts in the /src folder.

The src/routeTree.gen.ts file contains the complete route tree that is used by the router internally. The file is generated and gets updated automatically as long as the development server is running, so you do not need to create or update the file yourself.

The plugin automatically watches for file changes in the src/routes folder and automatically updates or regenerates the src/routeTree.gen.ts file when a new route file is added, an existing one is renamed or a route is nested in folders.

Also, notice how all the errors we were getting across the files disappeared after starting the development server to generate the route tree.

Open the application in the browser or go to http://localhost:5173 to see the running application and the defined routes working.

Basic TanStack Router implementation

You should also have the TanStack Router developer tools installed and added in the previous sections.

The route tree is so helpful and does more than just automate route setup. It provides good developer experience benefits such as IntelliSense, type safety, and more. For example, when trying to define a route, the code editor (e.g., VS Code) will give you autocompletion for route paths. This removes guesswork and reduces bugs.

Route path auto-completion

Route Navigation

Now that we have our routes set up, let’s discuss how we can move between them in our application. TanStack Router offers four type-safe navigation approaches:

  • The Link component
  • The useNavigate hook
  • The Navigate component
  • The router.navigate() method

Let’s briefly take a look at each of these.

This is a declarative and the most common approach for navigation. We’ve already seen it in action in the root layout of our application, where we defined two links that reference the Home and Books pages.

<div className="p-2 flex gap-2">
  <Link to="/">Home</Link> <Link to="/books">Books</Link>
</div>

The <Link> component generates the traditional <a> tag with a valid href, which can be clicked to navigate to a new page.

Route parameters and search values can also be passed as shown below:

<Link to="/books/$bookId" params={{ bookId: "1" }}>
  Books
</Link>

The useNavigate Hook

The useNavigate hook allows programmatic navigation across the application. It is useful when you need to navigate based on some user action, like a button click or after form submission.

Here is a sample code snippet of how to use it:

import { useNavigate } from "@tanstack/react-router";

function Books() {
  const navigate = useNavigate();

  return (
    <div>
      <button onClick={() => navigate({ to: "/books" })}>Books</button>
    </div>
  );
}

The useNavigate() hook returns a navigate function that can be called to imperatively navigate to a different route.

The Navigate Component

The Navigate component provides a great way to handle client-side redirections. It comes in handy in cases where we want to navigate immediately when a component renders. Rather than relying on the useNavigate() hook and then bringing in useEffect, we can just use the Navigate component.

Here is an example of how to use it:

function Books() {
  return <Navigate to="/books/$bookId" params={{ bookId: "1" }} />;
}

The router.navigate() Method

The router.navigate() method works just like the navigate function returned by useNavigate, with the addition that it is available anywhere the router instance is available. Therefore, it is a great way to navigate imperatively from anywhere within the application, including outside the framework.

Here is an example of how to use it:

router.navigate({ to: "/books" });

router here is the instance of the TanStack Router created from:

const router = createRouter({ routeTree });

Each of these navigation approaches accepts additional options that allow flexibility based on different use cases.

Dynamic Routes

The concept of dynamic routing here is the same as what we are all familiar with (i.e., defining URL segments that act like a variable, which enables the rendering of different content based on the dynamic parameter).

In file-based routing with TanStack Router, dynamic segments are created by starting the parameter name with a $ inside the filename. The dynamic parameter can then be accessed inside the route using the useParams method or in a loader.

To see how this works, let’s define a dynamic route for individual books in our application. Create a $bookId.tsx file in the src/routes/books/ folder and add the following to it:

import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/books/$bookId")({
  component: BookDetail,
});

function BookDetail() {
  const { bookId } = Route.useParams();

  return (
    <div>
      <h2 className="">Book Detail</h2>
      <p>
        You're viewing the book with ID: <strong>{bookId}</strong>
      </p>
    </div>
  );
}

Here, we registered the route and informed TanStack Router to expect a bookId param. If you’ve followed all steps up to this point, you should get auto-completion when you create the file.

Then we extracted the bookId param from the useParams method.

To be able to navigate to this dynamic route, let’s modify the src/routes/books/index.tsx file as shown below:

import { createFileRoute, Link } from "@tanstack/react-router";

export const Route = createFileRoute("/books/")({
  component: Books,
});

const books = [
  { id: "1", title: "Atomic Habits" },
  { id: "2", title: "Deep Work" },
  { id: "3", title: "A Random Walk Down Wall Street" },
];

function Books() {
  return (
    <div>
      <h2>Book List</h2>
      <ul className="list-disc ml-5 space-y-1">
        {books.map((book) => (
          <li key={book.id}>
            <Link to="/books/$bookId" params={{ bookId: book.id }}>
              {book.title} - (#{book.id})
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

In the code above, we created a dummy books array and mapped over it to render each book as a link that points to /books/$bookId. We also passed the correct params object to the link.

You can save the files and view the application in your browser.

Dynamic routes

Understanding Loaders

In addition to the component property that we’ve been using in the createFileRoute function, TanStack Router allows us to configure several other options when defining a route. Some of the most common ones include loader for fetching or preparing data before the route renders, errorComponent for handling errors, pendingComponent for displaying a loading user interface and so on.

In this article, we will focus on the loader option. Loaders are functions that run before a route’s component is rendered. They’re commonly used to fetch data, read params, or do any preparations required by the component to be rendered. The returned value from the loader can be accessed in the rendered component using the useLoaderData() hook.

To see how loaders work, open the src/routes/books/$bookId.tsx file and update the code as shown below:

import { createFileRoute } from "@tanstack/react-router";

const books = [
  { id: "1", title: "Atomic Habits" },
  { id: "2", title: "Deep Work" },
  { id: "3", title: "A Random Walk Down Wall Street" },
];

export const Route = createFileRoute("/books/$bookId")({
  loader: ({ params }) => {
    const book = books.find((b) => b.id === params.bookId);
    if (!book) {
      throw new Error("Book not found");
    }
    return book;
  },
  component: BookDetail,
});

function BookDetail() {
  const book = Route.useLoaderData();

  return (
    <div>
      <h2 className="">Book Detail</h2>
      <p>
        You're viewing the book with ID: <strong>{book.id}</strong>
      </p>
    </div>
  );
}

In the code above, we defined a loader and created a simple validation check inside it. When an invalid ID is passed as a parameter, an error is thrown; otherwise, the book object becomes accessible via the useLoaderData() method inside the BookDetail component.

Loaders are quite useful and offer a lot of benefits. They help preload data, separate concerns and, importantly, the loader return type and the useLoaderData() function are fully typed.

Error Handling in Routes

As mentioned earlier, TanStack Router also supports an errorComponent option in each route configuration. This is used to define a fallback interface when an error is thrown, either inside the route component or in its loader.

This makes it easy to provide route-specific error boundaries and prevent a single route failure from crashing the entire application.

Since we are already throwing an error when we try to access a book with an invalid ID in the demo application, let’s handle the error properly.

Add a components folder to the src/ folder and add an ErrorBoundary.tsx file to it. Open the file and add the following to it:

export default function ErrorBoundary({ error }: { error: Error }) {
  return (
    <div>
      <h2>Something went wrong, from the Error Boundary!</h2>
      <p>{error.message}</p>
    </div>
  );
}

Next, update the src/routes/books/$bookId.tsx file as shown below:

import { createFileRoute } from "@tanstack/react-router";
import ErrorBoundary from "../../components/ErrorBoundary";

const books = [
  { id: "1", title: "Atomic Habits" },
  { id: "2", title: "Deep Work" },
  { id: "3", title: "A Random Walk Down Wall Street" },
];

export const Route = createFileRoute("/books/$bookId")({
  errorComponent: ErrorBoundary,
  loader: ({ params }) => {
    const book = books.find((b) => b.id === params.bookId);
    if (!book) {
      throw new Error("Book not found");
    }
    return book;
  },
  component: BookDetail,
});

function BookDetail() {
  const book = Route.useLoaderData();

  return (
    <div>
      <h2 className="">Book Detail</h2>
      <p>
        You're viewing the book with ID: <strong>{book.id}</strong>
      </p>
    </div>
  );
}

Here, we imported the ErrorBoundary component we created and added it as the value to the errorComponent option. This prevents the application from crashing when the parameter is an invalid ID.

Error handling in routes

Protected Routes

In most real-world applications, we want to be able to protect users from some routes of an application based on some authentication or authorization factors. Some of these routes may include dashboard-related routes, admin routes and so on.

TanStack Router makes it easy to protect routes using the beforeLoad function. It runs before the route loads and even before the loader, and can be used to redirect users based on specified conditions.

Here is an example of how to implement it:

export const Route = createFileRoute("/dashboard")({
  beforeLoad: () => {
    //authentication check
    if (!isUserAuthenticated()) {
      throw redirect({ to: "/login" });
    }
  },
  component: Dashboard,
});

Here is a basic beforeLoad definition that checks the user’s authentication status and redirects if not authenticated.

Redirection inside the beforeLoad function can also be done while preserving the original path as a query parameter.

Here is what that looks like:

export const Route = createFileRoute("/dashboard")({
  beforeLoad: () => {
    //authentication check
    if (!isUserAuthenticated()) {
      throw redirect({ to: "/login" });
    }
  },
  component: Dashboard,
});

Here, redirection is done to the /login route if the user is not authenticated and back to the previous location after authentication.

Conclusion

TanStack Router is a powerful client-side routing library, and in this article, we looked at setting up a TanStack Router project and defining routes using the file-based approach, handling dynamic routes, loaders, error boundaries and even protected routes.

There is still much more to explore with the library. TanStack Router offers more features like advanced search params, route context, and more. If you’re interested in digging deeper, you should check out the official TanStack Router documentation.


About the Author

Christian Nwamba

Chris Nwamba is a Senior Developer Advocate at AWS focusing on AWS Amplify. He is also a teacher with years of experience building products and communities.

Related Posts

Comments

Comments are disabled in preview mode.