TanStack Router
Expert assistance with TanStack Router - Type-safe routing for React.
Overview
TanStack Router is a fully type-safe React router:
-
File-Based Routing: Automatic route generation from file structure
-
Type Safety: Full TypeScript inference for params, search, and more
-
Code Splitting: Automatic route-based code splitting
-
Data Loading: Built-in data loaders
-
Search Params: Type-safe search parameter handling
Installation
npm install @tanstack/react-router npm install --save-dev @tanstack/router-vite-plugin
Basic Setup
Vite Configuration
// vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { TanStackRouterVite } from '@tanstack/router-vite-plugin';
export default defineConfig({ plugins: [ react(), TanStackRouterVite(), ], });
Root Route
// src/routes/__root.tsx import { createRootRoute, Outlet } from '@tanstack/react-router';
export const Route = createRootRoute({ component: () => ( <> <div> <nav> <Link to="/">Home</Link> <Link to="/certificates">Certificates</Link> </nav> </div> <hr /> <Outlet /> </> ), });
Index Route
// src/routes/index.tsx import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/')({ component: () => <div>Home Page</div>, });
File-Based Routing
src/routes/ ├── __root.tsx -> / ├── index.tsx -> / ├── certificates/ │ ├── index.tsx -> /certificates │ └── $id.tsx -> /certificates/:id ├── cas/ │ ├── index.tsx -> /cas │ ├── $id.tsx -> /cas/:id │ └── $id.edit.tsx -> /cas/:id/edit └── audit.tsx -> /audit
Dynamic Routes
// src/routes/certificates/$id.tsx import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/certificates/$id')({ component: CertificateDetail, });
function CertificateDetail() { const { id } = Route.useParams(); // Type-safe! return <div>Certificate: {id}</div>; }
Nested Routes
// src/routes/certificates.tsx (layout) import { createFileRoute, Outlet } from '@tanstack/react-router';
export const Route = createFileRoute('/certificates')({ component: () => ( <div> <h1>Certificates</h1> <Outlet /> {/* Render child routes */} </div> ), });
// src/routes/certificates/index.tsx export const Route = createFileRoute('/certificates/')({ component: () => <div>Certificate List</div>, });
// src/routes/certificates/$id.tsx export const Route = createFileRoute('/certificates/$id')({ component: () => { const { id } = Route.useParams(); return <div>Certificate {id}</div>; }, });
Navigation
import { Link, useNavigate } from '@tanstack/react-router';
function MyComponent() { const navigate = useNavigate();
return ( <> {/* Link component */} <Link to="/certificates">Certificates</Link>
{/* With params */}
<Link to="/certificates/$id" params={{ id: '123' }}>
Certificate 123
</Link>
{/* With search params */}
<Link to="/certificates" search={{ filter: 'active' }}>
Active Certificates
</Link>
{/* Programmatic navigation */}
<button onClick={() => navigate({ to: '/certificates' })}>
Go to Certificates
</button>
{/* Navigate with params */}
<button onClick={() => navigate({
to: '/certificates/$id',
params: { id: '123' },
})}>
View Certificate
</button>
</>
); }
Search Params
Define Search Schema
import { createFileRoute } from '@tanstack/react-router'; import { z } from 'zod';
const searchSchema = z.object({ filter: z.enum(['all', 'active', 'revoked']).optional().default('all'), page: z.number().optional().default(1), pageSize: z.number().optional().default(10), });
export const Route = createFileRoute('/certificates/')({ validateSearch: searchSchema, component: CertificateList, });
function CertificateList() { const { filter, page, pageSize } = Route.useSearch(); // Type-safe!
return ( <div> <p>Filter: {filter}, Page: {page}</p> </div> ); }
Update Search Params
import { useNavigate } from '@tanstack/react-router';
function FilterButtons() { const navigate = useNavigate({ from: '/certificates' }); const search = Route.useSearch();
return ( <> <button onClick={() => navigate({ search: (prev) => ({ ...prev, filter: 'active' }), })}> Active </button>
<button onClick={() => navigate({
search: { filter: 'revoked', page: 1 },
})}>
Revoked
</button>
</>
); }
Route Loaders
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/certificates/$id')({ loader: async ({ params }) => { const certificate = await fetchCertificate(params.id); return { certificate }; }, component: CertificateDetail, });
function CertificateDetail() { const { certificate } = Route.useLoaderData(); return <div>{certificate.subject}</div>; }
Loader with Context
// src/router.tsx import { createRouter } from '@tanstack/react-router';
const router = createRouter({ routeTree, context: { queryClient, // React Query client auth, // Auth context }, });
// Route with context export const Route = createFileRoute('/certificates/$id')({ loader: async ({ params, context }) => { const certificate = await context.queryClient.fetchQuery({ queryKey: ['certificate', params.id], queryFn: () => fetchCertificate(params.id), }); return { certificate }; }, });
Lazy Loading
// src/routes/certificates/$id.lazy.tsx import { createLazyFileRoute } from '@tanstack/react-router';
export const Route = createLazyFileRoute('/certificates/$id')({ component: CertificateDetail, });
// Component defined in same file (lazy loaded) function CertificateDetail() { const { id } = Route.useParams(); return <div>Certificate: {id}</div>; }
Error Handling
export const Route = createFileRoute('/certificates/$id')({ loader: async ({ params }) => { const certificate = await fetchCertificate(params.id); if (!certificate) { throw new Error('Certificate not found'); } return { certificate }; }, errorComponent: ({ error }) => ( <div> <h2>Error!</h2> <p>{error.message}</p> </div> ), component: CertificateDetail, });
Pending/Loading States
export const Route = createFileRoute('/certificates/$id')({ loader: async ({ params }) => { const certificate = await fetchCertificate(params.id); return { certificate }; }, pendingComponent: () => <div>Loading certificate...</div>, component: CertificateDetail, });
// Or use hook function CertificateDetail() { const { certificate } = Route.useLoaderData(); const isPending = Route.useLoaderPending();
if (isPending) return <div>Loading...</div>;
return <div>{certificate.subject}</div>; }
Route Guards
export const Route = createFileRoute('/admin')({ beforeLoad: async ({ context }) => { if (!context.auth.isAuthenticated) { throw redirect({ to: '/login' }); } }, component: AdminPanel, });
Router Setup
// src/router.tsx import { createRouter } from '@tanstack/react-router'; import { routeTree } from './routeTree.gen';
export const router = createRouter({ routeTree, defaultPreload: 'intent', // Preload on hover defaultPreloadStaleTime: 0, });
declare module '@tanstack/react-router' { interface Register { router: typeof router; } }
// src/main.tsx import { RouterProvider } from '@tanstack/react-router'; import { router } from './router';
function App() { return <RouterProvider router={router} />; }
Best Practices
-
File Structure: Organize routes by feature/domain
-
Type Safety: Leverage full type inference
-
Search Params: Define schemas for search params
-
Code Splitting: Use lazy routes for large components
-
Error Boundaries: Implement error components
-
Loading States: Show pending UI for better UX
-
Preloading: Use intent-based preloading
-
Route Guards: Protect routes with beforeLoad
-
Loaders: Fetch data in loaders, not components
-
Context: Share context through router
Resources
-
Documentation: https://tanstack.com/router