[ PROMPT_NODE_25281 ]
Tanstack Router
[ SKILL_DOCUMENTATION ]
# TanStack Router TypeScript Patterns
TanStack Router provides full type safety for routes, params, search params, and loader data.
## Basic Route Definition
```typescript
import { createRoute, createRootRoute } from '@tanstack/react-router';
// Root route
const rootRoute = createRootRoute({
component: RootLayout,
});
// Basic route
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: HomePage,
});
// Route tree
const routeTree = rootRoute.addChildren([indexRoute]);
// Router
const router = createRouter({ routeTree });
// Type for router - use in app
type Router = typeof router;
```
## Route with Params
```typescript
import { createRoute } from '@tanstack/react-router';
import { useParams } from '@tanstack/react-router';
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId',
component: UserPage,
});
function UserPage() {
const { userId } = useParams({ from: userRoute.id });
// userId is typed as string
return
);
}
async function fetchUser(id: string): Promise {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
```
## Loader with Search Params
```typescript
const productsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/products',
component: ProductsPage,
validateSearch: z.object({
category: z.string().optional(),
page: z.number().default(1),
}),
loader: async ({ search }) => {
// search is typed from validateSearch
const products = await fetchProducts({
category: search.category,
page: search.page,
});
return { products };
},
});
function ProductsPage() {
const { products } = useLoaderData({ from: productsRoute.id });
const search = useSearch({ from: productsRoute.id });
return (
);
}
```
## Type-Safe Navigation
```typescript
import { useNavigate, Link } from '@tanstack/react-router';
function ProductList() {
const navigate = useNavigate();
const goToProduct = (productId: string) => {
navigate({
to: '/products/$productId',
params: { productId }, // Type-checked
search: { tab: 'reviews' }, // Type-checked against validateSearch
});
};
return (
);
}
```
## Before Load Hook
```typescript
const protectedRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/dashboard',
beforeLoad: async ({ location }) => {
const isAuthenticated = await checkAuth();
if (!isAuthenticated) {
throw redirect({
to: '/login',
search: { redirect: location.href },
});
}
return { user: await getCurrentUser() };
},
component: Dashboard,
});
function Dashboard() {
const { user } = useLoaderData({ from: protectedRoute.id });
// user available from beforeLoad
return
);
}
```
## Pending Component
```typescript
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId',
component: UserPage,
pendingComponent: UserPageSkeleton,
loader: async ({ params }) => {
const user = await fetchUser(params.userId);
return { user };
},
});
function UserPageSkeleton() {
return (
);
}
```
## Router Context
Share data across all routes.
```typescript
type RouterContext = {
auth: { user: User | null };
queryClient: QueryClient;
};
const rootRoute = createRootRoute({
component: RootLayout,
});
const router = createRouter({
routeTree,
context: {
auth: { user: null },
queryClient: new QueryClient(),
},
});
// Access in route
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId',
beforeLoad: ({ context }) => {
// context typed as RouterContext
console.log(context.auth.user);
},
});
```
## Route Masks
Hide actual URL structure.
```typescript
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId',
});
// Navigate with mask
navigate({
to: '/users/$userId',
params: { userId: '123' },
mask: {
to: '/profile',
},
});
// URL shows /profile but renders /users/123
```
## Search Param Middleware
Transform search params before validation.
```typescript
const productsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/products',
validateSearch: z.object({
tags: z.array(z.string()).default([]),
}),
loaderDeps: ({ search }) => ({ tags: search.tags }),
loader: async ({ deps }) => {
const products = await fetchProducts({ tags: deps.tags });
return { products };
},
});
// URL: /products?tags=electronics&tags=sale
// search.tags = ['electronics', 'sale']
```
## Type Helpers
```typescript
import type { RouteIds, RouteById } from '@tanstack/react-router';
// Get all route IDs
type AllRouteIds = RouteIds;
// Get specific route type
type UserRoute = RouteById;
// Extract params type
type UserParams = UserRoute['types']['allParams'];
// Extract search type
type UserSearch = UserRoute['types']['fullSearchSchema'];
```
## Preloading Routes
```typescript
import { useRouter } from '@tanstack/react-router';
function ProductCard({ productId }: { productId: string }) {
const router = useRouter();
const preloadProduct = () => {
router.preloadRoute({
to: '/products/$productId',
params: { productId },
});
};
return (
View Product
);
}
```
## Route Matching
```typescript
import { useMatches, useMatch } from '@tanstack/react-router';
function Navigation() {
const matches = useMatches();
// Array of all matched routes
const userMatch = useMatch({ from: userRoute.id, shouldThrow: false });
// userMatch is typed, null if not matched
return (
);
}
```
## File-Based Routing (Code Generation)
```typescript
// routes/__root.tsx
export const Route = createRootRoute({
component: RootLayout,
});
// routes/index.tsx
export const Route = createFileRoute('/')({
component: HomePage,
});
// routes/users/$userId.tsx
export const Route = createFileRoute('/users/$userId')({
component: UserPage,
loader: async ({ params }) => {
const user = await fetchUser(params.userId);
return { user };
},
});
// Generate route tree with CLI
// npm run generate-routes
// Import generated routes
import { routeTree } from './routeTree.gen';
const router = createRouter({ routeTree });
```
## Integration with React Query
```typescript
import { queryOptions, useQuery } from '@tanstack/react-query';
const userQueryOptions = (userId: string) =>
queryOptions({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId',
loader: ({ context, params }) => {
// Prefetch with React Query
return context.queryClient.ensureQueryData(userQueryOptions(params.userId));
},
component: UserPage,
});
function UserPage() {
const { userId } = useParams({ from: userRoute.id });
// Use same query options in component
const { data: user } = useQuery(userQueryOptions(userId));
return
User ID: {userId}
;
}
// Multiple params
const postRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId/posts/$postId',
component: PostPage,
});
function PostPage() {
const { userId, postId } = useParams({ from: postRoute.id });
// Both typed as string
return Post {postId} by user {userId}
;
}
```
## Search Params with Validation
```typescript
import { z } from 'zod';
const productsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/products',
component: ProductsPage,
validateSearch: z.object({
category: z.string().optional(),
sortBy: z.enum(['price', 'name', 'rating']).default('name'),
page: z.number().int().positive().default(1),
perPage: z.number().int().positive().default(20),
}),
});
function ProductsPage() {
const search = useSearch({ from: productsRoute.id });
// search is typed from Zod schema:
// {
// category?: string;
// sortBy: 'price' | 'name' | 'rating';
// page: number;
// perPage: number;
// }
return (
Category: {search.category || 'All'}
Sort by: {search.sortBy}
Page: {search.page}
);
}
```
## Loader Data
```typescript
type User = {
id: string;
name: string;
email: string;
};
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId',
component: UserPage,
loader: async ({ params }) => {
const user = await fetchUser(params.userId);
return { user };
},
});
function UserPage() {
const { user } = useLoaderData({ from: userRoute.id });
// user typed as User
return (
{user.name}
{user.email}
Products - {search.category || 'All'}
-
{products.map((p) => (
- {p.name} ))}
{products.map((product) => (
{product.name}
))}
);
}
```
## Search Params Manipulation
```typescript
import { useNavigate, useSearch } from '@tanstack/react-router';
const productsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/products',
validateSearch: z.object({
category: z.string().optional(),
sortBy: z.enum(['price', 'name']).default('name'),
page: z.number().default(1),
}),
});
function ProductFilters() {
const navigate = useNavigate({ from: productsRoute.id });
const search = useSearch({ from: productsRoute.id });
const updateCategory = (category: string) => {
navigate({
search: (prev) => ({
...prev,
category, // Type-checked
page: 1, // Reset page
}),
});
};
const updateSort = (sortBy: 'price' | 'name') => {
navigate({
search: (prev) => ({ ...prev, sortBy }),
});
};
return (
updateCategory(e.target.value)}>
All Categories
Electronics
Books
updateSort(e.target.value as 'price' | 'name')}>
Name
Price
);
}
```
## Nested Routes
```typescript
const usersRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users',
component: UsersLayout,
});
const usersIndexRoute = createRoute({
getParentRoute: () => usersRoute,
path: '/',
component: UsersList,
});
const userDetailRoute = createRoute({
getParentRoute: () => usersRoute,
path: '$userId',
component: UserDetail,
});
const routeTree = rootRoute.addChildren([
usersRoute.addChildren([
usersIndexRoute,
userDetailRoute,
]),
]);
// Layout component with outlet
function UsersLayout() {
return (
Users
{/* Renders child route */}Welcome, {user.name}
;
}
```
## Error Handling
```typescript
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId',
component: UserPage,
errorComponent: UserErrorPage,
loader: async ({ params }) => {
const user = await fetchUser(params.userId);
if (!user) {
throw new Error('User not found');
}
return { user };
},
});
function UserErrorPage({ error }: { error: Error }) {
return (
Error
{error.message}
Back to users{user?.name}
;
}
```
Source: claude-code-templates (MIT). See About Us for full credits.