Recommended resources
The freely available resources this lesson will direct you to.
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.
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:
- Path Autocompletion & Validation: The
toprop offers autocomplete for all valid paths, and TypeScript will raise an error if you try to link to a route that doesn't exist. - Parameter Enforcement: If a destination route requires a path parameter (e.g.,
/posts/$postId), the<Link>component is statically typed to require aparamsprop. 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.
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:
- The
parseParamsfunction receives the raw string parameters from the URL. - We use
z.number().int().parse()to validate thatparams.postIdcan be converted to an integer. IfpostIdwere "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). - Because we defined this parsing logic, Tanstack Router infers that the output of
Route.useParams()will now providepostIdas anumber. 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:
- We create a
zodschema 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, use1". - We pass this schema to
validateSearchusing thezodValidatoradapter. This adapter correctly handles Zod's input/output types, making it so thatdefaultvalues don't make parameters mandatory when creating links. - The
Route.useSearch()hook now returns an object that is guaranteed to match our schema.pageis anumberandsortis either'asc'or'desc'. No more manual parsing or default value checks in the component. - The
<Link>component'ssearchprop 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>: Theto,params, andsearchprops 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.