Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Type-Safe Routing with Tanstack Router

Hello! Let's continue our journey into Tanstack Router.

In our last lesson, we laid the groundwork by integrating Tanstack Router into a React project and defining routes using its file-based conventions. The key outcome was the auto-generated routeTree.gen.ts file, which creates a typed representation of our application's routes.

Today, we'll leverage that generated type information to its full potential. The learning outcome for this lesson is to implement type-safe navigation and parameter handling using Tanstack Router's APIs. We will explore how Tanstack Router transforms URL parameters from simple strings into a reliable, typed state management system. This is where the developer experience of the library truly shines.

1. Type-Safe Navigation with the <Link> Component

In the previous lesson, we used the <Link> component for basic navigation. Now, let's explore its type-safety features. Because the router plugin generates types for all valid routes, the <Link> component becomes "aware" of your application's structure.

Why I Love TanStack Router

This short clip from the "Why I Love TanStack Router" video by Better Stack provides an excellent visual demonstration of the type-safety features built into the <Link> component.

Watch type-safe links. Pay close attention to how the editor provides autocomplete for the to prop and flags errors for non-existent routes or missing parameters.

As you saw, this provides two immediate benefits:

  1. Path Autocompletion & Validation: The to prop offers autocomplete for all valid paths, and TypeScript will raise an error if you try to link to a route that doesn't exist.
  2. Parameter Enforcement: If a destination route requires a path parameter (e.g., /posts/$postId), the <Link> component is statically typed to require a params prop. TypeScript will ensure you provide all necessary parameters.

Let's look at the code. Assuming you have a route at src/routes/posts/$postId.tsx, navigating to it would look like this:

import { Link } from '@tanstack/react-router'

function PostList() {
  const posts = [{ id: '1', title: 'First Post' }, { id: '2', title: 'Second Post' }];

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          {/* TypeScript will enforce that `params` is provided and has a `postId` property */}
          <Link
            to="/posts/$postId"
            params={{
              postId: post.id,
            }}
          >
            {post.title}
          </Link>
        </li>
      ))}
    </ul>
  );
}

Attempting to link without the params prop would result in a compile-time error, preventing broken links in your application. This is a direct benefit of the routeTree.gen.ts file we generated in the last lesson.

2. Validating and Parsing Path Parameters

While the URL path /posts/123 treats 123 as a string, your application logic likely needs it as a number. Manually parsing this in your component is error-prone. Tanstack Router allows you to define parsing and validation logic directly in the route definition.

This is done using the parseParams option in createFileRoute.

Why I Love TanStack Router

This next clip from the same video demonstrates how to use Zod, a popular schema validation library, to parse and validate path parameters.

Watch path parameter validation. Notice how the parseParams option is used in the route definition and how this changes the type of the parameter returned by the useParams hook.

Here is how you would implement this for our /posts/$postId route.

First, install Zod:

npm install zod

Then, update your route file to include validation:

// src/routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'

export const Route = createFileRoute('/posts/$postId')({
  // 1. Define parsing logic for the route's parameters
  parseParams: (params) => ({
    postId: z.number().int().parse(Number(params.postId)),
  }),
  component: PostComponent,
})

function PostComponent() {
  // 2. The type of `postId` is now `number`, not `string`
  const { postId } = Route.useParams()

  return <div>Post ID: {postId}</div>
}

How it works:

  1. The parseParams function receives the raw string parameters from the URL.
  2. We use z.number().int().parse() to validate that params.postId can be converted to an integer. If postId were "abc", Zod would throw an error, which Tanstack Router would catch and display your configured error component for that route (a topic for a future lesson).
  3. Because we defined this parsing logic, Tanstack Router infers that the output of Route.useParams() will now provide postId as a number. Your component can now trust the type and value of the parameter without any internal checks.

3. Mastering Type-Safe Search Parameters

Search parameters (e.g., ?page=2&sort=asc) are a powerful way to manage UI state in the URL. However, they are notoriously difficult to manage. Tanstack Router elevates them to a first-class, type-safe state management system.

Search Params | TanStack Router React Docs

The official documentation explains the philosophy behind TanStack Router's advanced handling of search parameters. It's worth understanding the 'why' before we get to the 'how'.

Read the sections 'Why not just use URLSearchParams?' (read the platform limitations) and 'Search Params, the "OG" State Manager' (read about URL global state). This will contextualize the features we are about to implement.

The core mechanism for this is the validateSearch option in createFileRoute. It works similarly to parseParams but is designed for the more complex nature of search queries.

Defining a Search Schema

Let's enhance our /posts index route to support pagination and sorting.

Search Params | TanStack Router React Docs

This documentation section details how to use a validation library like Zod with the validateSearch option. It's the most robust way to handle search params.

Read the sections 'Validating and Typing Search Params' (validation basics) and the sub-section on using Zod (Zod integration). Focus on how a Zod schema is defined and passed to validateSearch, and the purpose of the zodValidator adapter.

Based on the documentation, here's how to implement it.

First, you may need to install the Zod adapter:
npm install @tanstack/zod-adapter

Now, let's define a schema in src/routes/posts/index.tsx:

// src/routes/posts/index.tsx
import { createFileRoute, Link } from '@tanstack/react-router'
import { z } from 'zod'
import { zodValidator } from '@tanstack/zod-adapter'

// 1. Define the schema for search parameters
const postsSearchSchema = z.object({
  page: z.number().min(1).catch(1), // Default to page 1 if invalid
  sort: z.enum(['asc', 'desc']).catch('desc'), // Default to 'desc'
})

export const Route = createFileRoute('/posts/')({
  // 2. Validate search params against the schema
  validateSearch: zodValidator(postsSearchSchema),
  component: PostsIndex,
})

function PostsIndex() {
  // 3. Access fully typed and validated search params
  const { page, sort } = Route.useSearch()

  return (
    <div>
      <h3>Posts - Page {page}, Sort {sort}</h3>
      {/* 4. Link component is now type-aware of search params */}
      <div className="flex gap-2">
        <Link
          search={(prev) => ({ ...prev, page: prev.page + 1 })}
        >
          Next Page
        </Link>
        <Link
          search={{ page: 1, sort: 'asc' }}
          className="[&.active]:font-bold"
        >
          Sort Asc
        </Link>
      </div>
    </div>
  )
}

Dissecting this powerful pattern:

  1. We create a zod schema that defines the shape, type, and default/fallback values for our search state. z.number().catch(1) means "try to parse a number, but if it fails or is missing, use 1".
  2. We pass this schema to validateSearch using the zodValidator adapter. This adapter correctly handles Zod's input/output types, making it so that default values don't make parameters mandatory when creating links.
  3. The Route.useSearch() hook now returns an object that is guaranteed to match our schema. page is a number and sort is either 'asc' or 'desc'. No more manual parsing or default value checks in the component.
  4. The <Link> component's search prop is also now fully typed. It knows which keys are valid (page, sort) and what types they accept. The functional update form (search={prev => ...}) is extremely useful for modifying only part of the search state.

4. Imperative Navigation with useNavigate

While the <Link> component is preferred for user-facing navigation, you sometimes need to navigate programmatically, for instance, after a form submission. For this, you can use the useNavigate hook. It uses the same type-safe API as <Link>.

Navigation | TanStack Router React Docs

The documentation provides a clear explanation of the useNavigate hook for imperative navigation.

Read the short section on useNavigate. Note that it accepts the same options object (to, params, search) as the <Link> component.

Here's a quick example of its usage:

import { useNavigate } from '@tanstack/react-router'

function CreatePostForm() {
  const navigate = useNavigate({ from: '/posts/' })

  const handleSubmit = async (formData) => {
    // const newPost = await api.createPost(formData)
    const newPost = { id: '123' }; // mock response

    // Navigate to the new post's page after creation
    navigate({
      to: '/posts/$postId',
      params: {
        postId: newPost.id,
      },
    })
  }

  // ... form JSX
}

The navigate function is fully type-safe, just like the <Link> component, ensuring you provide the correct path and required parameters.

Conclusion

In this lesson, we unlocked the core developer experience of Tanstack Router: comprehensive type safety for navigation and URL parameters. By defining validation schemas at the route level, we can treat URL state with the same reliability as internal application state.

Key Takeaways:

  • Type-Safe <Link>: The to, params, and search props are all type-checked against your generated route tree, preventing broken links at compile time.
  • parseParams: Use this route option with a library like Zod to validate and parse path parameters (e.g., string to number), ensuring your component receives data in the correct format.
  • validateSearch: This powerful option allows you to define a schema for search parameters, including types, validation rules, and default/fallback values.
  • useParams & useSearch: These hooks provide type-safe access to the validated parameters within your components.
  • useNavigate: This hook provides a type-safe function for performing imperative navigation as a side effect.

In our next lesson, we will build on these concepts to structure applications with nested routes and shared layouts. We'll see how to create complex page structures where parts of the UI persist across navigations, a fundamental pattern for building large-scale applications.

Can't find a good explanation? Sign up and we'll make it for you

Sign up