TanStack Router cheatsheet for React from first routes and navigation to typed search params, loaders, auth, TanStack Query integration, and advanced routing patterns.
TanStack Router Cheatsheet
Section titled “TanStack Router Cheatsheet”TanStack Router is a type-safe router for React applications. It handles matching URLs to UI, nested layouts, navigation, URL parameters, route loaders, pending and error states, and preloading.
This page uses the current TanStack Router v1 React API. TypeScript is strongly recommended because inferred route, parameter, search, and navigation types are a major benefit of the library.
| Problem | Router Tool |
|---|---|
| Display a component for a URL | Route |
| Render child screens inside a layout | <Outlet /> |
| Navigate without a full reload | <Link /> / useNavigate() |
Read /posts/123 | Path params |
Store filters such as ?page=2 | Validated search params |
| Load before rendering a route | loader |
| Gate a private section | beforeLoad and redirect |
| Share services with loaders | Router context |
1. Install and Choose a Route Style
Section titled “1. Install and Choose a Route Style”Install the React router package:
npm install @tanstack/react-routerOptional development tools:
npm install @tanstack/react-router-devtoolsTanStack Router supports two styles:
| Style | Description | Good Starting Point |
|---|---|---|
| File-based | Files under src/routes/ generate the route tree | Most new applications |
| Code-based | Build the route tree directly in TypeScript | Small apps or custom configuration |
The following sections begin with file-based routing, then show code-based equivalents later.
2. File-Based Setup with Vite
Section titled “2. File-Based Setup with Vite”For file-based route generation in a Vite React app, install the plugin:
npm install -D @tanstack/router-pluginimport { defineConfig } from "vite";import react from "@vitejs/plugin-react";import { tanstackRouter } from "@tanstack/router-plugin/vite";
export default defineConfig({ plugins: [ tanstackRouter({ target: "react", autoCodeSplitting: true, }), react(), ],});Place the TanStack Router Vite plugin before the React plugin. With the default setup, route files live under src/routes/ and the generated tree is written to src/routeTree.gen.ts.
Basic File Structure
Section titled “Basic File Structure”src/ main.tsx router.tsx routeTree.gen.ts # Generated file; do not manually edit routes/ __root.tsx index.tsx about.tsx3. Your First File-Based Routes
Section titled “3. Your First File-Based Routes”Root Layout
Section titled “Root Layout”import { createRootRoute, Link, Outlet } from "@tanstack/react-router";import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
export const Route = createRootRoute({ component: RootLayout, notFoundComponent: () => <p>Page not found.</p>,});
function RootLayout() { return ( <> <header> <nav> <Link to="/">Home</Link>{" "} <Link to="/about">About</Link> </nav> </header> <main> <Outlet /> </main> <TanStackRouterDevtools /> </> );}Index Route
Section titled “Index Route”import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/")({ component: HomePage,});
function HomePage() { return <h1>Home</h1>;}Another Page
Section titled “Another Page”import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/about")({ component: AboutPage,});
function AboutPage() { return <h1>About</h1>;}For file-based routes, export the route instance using the name Route; the generator uses that convention.
4. Create and Render the Router
Section titled “4. Create and Render the Router”import { createRouter } from "@tanstack/react-router";import { routeTree } from "./routeTree.gen";
export const router = createRouter({ routeTree, defaultPreload: "intent",});
declare module "@tanstack/react-router" { interface Register { router: typeof router; }}import { StrictMode } from "react";import { createRoot } from "react-dom/client";import { RouterProvider } from "@tanstack/react-router";import { router } from "./router";
createRoot(document.getElementById("root")!).render( <StrictMode> <RouterProvider router={router} /> </StrictMode>,);| Setup Piece | Purpose |
|---|---|
routeTree | The generated or manually built route hierarchy |
createRouter | Creates the router instance |
RouterProvider | Renders matches and supplies router state to React |
Register declaration | Enables inferred type safety across router APIs |
defaultPreload: "intent" | Preloads links when the user indicates intent, such as hover |
Do not edit routeTree.gen.ts; regenerate it through the configured plugin or CLI.
5. Nested Routes and Layouts
Section titled “5. Nested Routes and Layouts”Route directories and filenames express nested URLs and layouts.
src/routes/ __root.tsx dashboard.tsx dashboard/ index.tsx settings.tsximport { createFileRoute, Link, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute("/dashboard")({ component: DashboardLayout,});
function DashboardLayout() { return ( <section> <h1>Dashboard</h1> <nav> <Link to="/dashboard">Overview</Link>{" "} <Link to="/dashboard/settings">Settings</Link> </nav> <Outlet /> </section> );}import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/dashboard/settings")({ component: () => <h2>Settings</h2>,});An <Outlet /> is the place where a matching child route renders.
6. Navigate with <Link>
Section titled “6. Navigate with <Link>”Use <Link> instead of a plain <a> for application routes so navigation is handled by the router.
import { Link } from "@tanstack/react-router";
function Navigation() { return ( <nav> <Link to="/">Home</Link> <Link to="/products">Products</Link> <Link to="/dashboard" activeProps={{ className: "active" }} preload="intent" > Dashboard </Link> </nav> );}Dynamic Destination
Section titled “Dynamic Destination”<Link to="/products/$productId" params={{ productId: product.id }}> {product.name}</Link>| Link Prop | Purpose |
|---|---|
to | Destination route pattern |
params | Values for path parameters |
search | URL search state |
hash | Fragment identifier |
preload | Whether/how to preload route dependencies |
activeProps | Props used when this link is active |
Pass dynamic values through params and search, rather than interpolating them into to.
7. Programmatic Navigation
Section titled “7. Programmatic Navigation”Use imperative navigation after actions such as saving a form:
import { useNavigate } from "@tanstack/react-router";import type { FormEvent } from "react";
function CreateProductForm() { const navigate = useNavigate();
async function handleSubmit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); const product = await createProduct();
navigate({ to: "/products/$productId", params: { productId: product.id }, }); }
return <form onSubmit={handleSubmit}>{/* fields */}</form>;}Redirect During Route Loading
Section titled “Redirect During Route Loading”import { redirect } from "@tanstack/react-router";
throw redirect({ to: "/login", search: { redirect: location.href, },});| Use | Tool |
|---|---|
| User clicks destination | <Link> |
| Code navigates after an event | useNavigate() |
| Loader/auth guard must leave the route | throw redirect(...) |
8. Path Parameters
Section titled “8. Path Parameters”A parameter segment begins with $.
src/routes/products/$productId.tsx// src/routes/products/$productId.tsximport { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/products/$productId")({ component: ProductPage,});
function ProductPage() { const { productId } = Route.useParams(); return <h1>Product: {productId}</h1>;}Linking to a Param Route
Section titled “Linking to a Param Route”<Link to="/products/$productId" params={{ productId: "keyboard-101" }}> View keyboard</Link>| URL | Route |
|---|---|
/products/keyboard-101 | /products/$productId |
/users/42/orders/9 | /users/$userId/orders/$orderId |
Path params identify a resource or hierarchy. Use search params for optional filters, tabs, sorting, and pagination.
9. Typed Search Parameters
Section titled “9. Typed Search Parameters”Search parameters are URL state, such as filters and pagination. Validate them at the route boundary so components receive predictable types.
import { createFileRoute } from "@tanstack/react-router";
type ProductSearch = { page: number; query: string; sort: "newest" | "price";};
export const Route = createFileRoute("/products")({ validateSearch: (search: Record<string, unknown>): ProductSearch => ({ page: Number(search.page ?? 1), query: typeof search.query === "string" ? search.query : "", sort: search.sort === "price" ? "price" : "newest", }), component: ProductsPage,});
function ProductsPage() { const search = Route.useSearch();
return ( <p> Search: {search.query || "all"}; page {search.page}; sort {search.sort} </p> );}For production applications, a schema validation library is a convenient way to parse and validate search data consistently.
Navigate with Search State
Section titled “Navigate with Search State”function ProductControls() { const navigate = Route.useNavigate();
return ( <> <Link to="/products" search={{ page: 1, query: "monitor", sort: "price" }} > Affordable monitors </Link>
<button type="button" onClick={() => navigate({ search: (previous) => ({ ...previous, page: previous.page + 1, }), }) } > Next page </button> </> );}| Belongs in URL Search | Usually Does Not |
|---|---|
| Search query and filters | Hover state |
| Sort order and table page | Open state of a tiny temporary tooltip |
| Selected shareable tab | Unsaved passwords |
| Values users can bookmark/share | Large private draft objects |
10. Route Loaders
Section titled “10. Route Loaders”A route loader retrieves data required for that route. The router can start loading before the route component renders.
// src/routes/products/$productId.tsximport { createFileRoute } from "@tanstack/react-router";
async function fetchProduct(productId: string) { const response = await fetch(`/api/products/${productId}`); if (!response.ok) throw new Error("Product not found"); return response.json() as Promise<{ id: string; name: string }>;}
export const Route = createFileRoute("/products/$productId")({ loader: ({ params }) => fetchProduct(params.productId), pendingComponent: () => <p>Loading product...</p>, errorComponent: ({ error }) => <p role="alert">{error.message}</p>, component: ProductPage,});
function ProductPage() { const product = Route.useLoaderData(); return <h1>{product.name}</h1>;}| Route Option | Use |
|---|---|
loader | Fetch route-required data |
pendingComponent | Render while slow route dependencies load |
errorComponent | Display errors in this route boundary |
loaderDeps | Declare search-dependent loader inputs |
staleTime | Control freshness for router-cached loader data |
Load Based on Search Params
Section titled “Load Based on Search Params”export const Route = createFileRoute("/products")({ validateSearch: (search: Record<string, unknown>) => ({ query: typeof search.query === "string" ? search.query : "", page: Number(search.page ?? 1), }), loaderDeps: ({ search }) => ({ query: search.query, page: search.page, }), loader: ({ deps }) => fetchProducts(deps), component: ProductsPage,});loaderDeps makes the data dependency explicit: changing a relevant URL filter causes the appropriate loader behavior.
11. Router Cache or TanStack Query?
Section titled “11. Router Cache or TanStack Query?”TanStack Router includes route-loader caching. TanStack Query offers a broader shared server-state cache and mutation workflow.
| Situation | Good Fit |
|---|---|
| Route needs data only while visiting that route | Router loader cache |
| Data shared across multiple routes/components | TanStack Query |
| Mutations, invalidation, optimistic UI needed | TanStack Query |
| Straightforward page preload with little shared API data | Router loader |
Do not fetch the same resource independently through both caches without an intentional strategy. A common integration is: the route loader primes TanStack Query, then components subscribe to the Query cache.
12. TanStack Query Integration
Section titled “12. TanStack Query Integration”Provide the Query Client in Router Context
Section titled “Provide the Query Client in Router Context”import { createRootRouteWithContext, Outlet,} from "@tanstack/react-router";import type { QueryClient } from "@tanstack/react-query";
export type RouterContext = { queryClient: QueryClient;};
export const Route = createRootRouteWithContext<RouterContext>()({ component: () => <Outlet />,});import { QueryClient } from "@tanstack/react-query";import { createRouter } from "@tanstack/react-router";import { routeTree } from "./routeTree.gen";
export const queryClient = new QueryClient();
export const router = createRouter({ routeTree, context: { queryClient },});
declare module "@tanstack/react-router" { interface Register { router: typeof router; }}import { QueryClientProvider } from "@tanstack/react-query";import { RouterProvider } from "@tanstack/react-router";import { queryClient, router } from "./router";
createRoot(document.getElementById("root")!).render( <QueryClientProvider client={queryClient}> <RouterProvider router={router} /> </QueryClientProvider>,);Ensure Query Data in a Loader
Section titled “Ensure Query Data in a Loader”import { queryOptions } from "@tanstack/react-query";
export const productOptions = (productId: string) => queryOptions({ queryKey: ["products", productId], queryFn: () => fetchProduct(productId), staleTime: 60_000, });// src/routes/products/$productId.tsximport { useQuery } from "@tanstack/react-query";import { createFileRoute } from "@tanstack/react-router";import { productOptions } from "../../features/products/queries";
export const Route = createFileRoute("/products/$productId")({ loader: ({ context, params }) => context.queryClient.ensureQueryData(productOptions(params.productId)), component: ProductPage,});
function ProductPage() { const { productId } = Route.useParams(); const { data: product } = useQuery(productOptions(productId)); return <h1>{product.name}</h1>;}This keeps one cache and one reusable request definition while allowing navigation to preload necessary data.
13. Router Context
Section titled “13. Router Context”Router context injects values that loaders and beforeLoad functions need, such as an authenticated user, API service, or QueryClient.
type Auth = { user: { id: string; name: string } | null; isLoading: boolean;};
type RouterContext = { auth: Auth;};
export const Route = createRootRouteWithContext<RouterContext>()({ component: () => <Outlet />,});Values produced by React hooks must be calculated in React and passed to RouterProvider; loaders cannot call hooks themselves:
function App() { const auth = useAuth();
return <RouterProvider router={router} context={{ auth }} />;}| Put in Router Context | Avoid |
|---|---|
| Authentication state needed for guards | Calling React hooks inside loaders |
| Query client or API client | Random component-only UI state |
| Feature flags affecting access | Hiding all dependencies in global imports |
14. Authenticated Routes
Section titled “14. Authenticated Routes”Use beforeLoad for access checks that must happen before child routes load.
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/_authenticated")({ beforeLoad: ({ context, location }) => { if (!context.auth.user) { throw redirect({ to: "/login", search: { redirect: location.href, }, }); } }, component: () => <Outlet />,});Child pages under an authenticated layout inherit the protection:
src/routes/ _authenticated.tsx _authenticated/ dashboard.tsx account.tsxAuthentication determines whether a user is signed in; authorization must still check whether that user may perform an operation, especially on the server.
15. Pending, Error, and Not Found UI
Section titled “15. Pending, Error, and Not Found UI”Provide user feedback for navigation states:
export const Route = createFileRoute("/reports")({ loader: loadReports, pendingComponent: () => <p>Loading reports...</p>, errorComponent: ({ error, reset }) => ( <section role="alert"> <p>{error.message}</p> <button type="button" onClick={reset}> Try again </button> </section> ), component: ReportsPage,});Root-level not found UI:
export const Route = createRootRoute({ component: RootLayout, notFoundComponent: () => ( <section> <h1>404</h1> <Link to="/">Return home</Link> </section> ),});| State | Provide |
|---|---|
| Route/code/data loading | Pending feedback or skeleton |
| Failed load | Error boundary with useful recovery |
| Unknown URL/resource | Not found experience |
| Protected route | Redirect or permission feedback |
16. Code-Based Routing
Section titled “16. Code-Based Routing”Routes can be built manually rather than generated from files:
import { createRootRoute, createRoute, createRouter, Link, Outlet, RouterProvider,} from "@tanstack/react-router";
const rootRoute = createRootRoute({ component: () => ( <> <nav> <Link to="/">Home</Link> <Link to="/about">About</Link> </nav> <Outlet /> </> ),});
const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: "/", component: () => <h1>Home</h1>,});
const aboutRoute = createRoute({ getParentRoute: () => rootRoute, path: "about", component: () => <h1>About</h1>,});
const routeTree = rootRoute.addChildren([indexRoute, aboutRoute]);
const router = createRouter({ routeTree });
declare module "@tanstack/react-router" { interface Register { router: typeof router; }}
export function App() { return <RouterProvider router={router} />;}| File-Based | Code-Based |
|---|---|
| Route tree generated from route modules | Route tree assembled by code |
| Less manual tree wiring | Maximum explicit control |
| Common choice for growing apps | Useful for small/custom route systems |
17. Preloading and Code Splitting
Section titled “17. Preloading and Code Splitting”Preloading requests a destination’s route dependencies before navigation completes.
const router = createRouter({ routeTree, defaultPreload: "intent", defaultPreloadStaleTime: 30_000,});<Link to="/reports" preload="intent"> Reports</Link>When configured for file-based routing, the Vite router plugin can enable automatic route code splitting:
tanstackRouter({ target: "react", autoCodeSplitting: true,});| Technique | Benefit |
|---|---|
| Intent preloading | Data/code begins loading on likely navigation |
| Route code splitting | Initial JavaScript bundle can remain smaller |
| Loader + Query prefetch | Destination data may already be cached |
Preload routes users are likely to visit; very aggressive preloading can waste network and device resources.
18. Search Params as Application State
Section titled “18. Search Params as Application State”URL state is especially valuable for table and search screens:
type SearchState = { query: string; page: number; category?: string;};
export const Route = createFileRoute("/catalog")({ validateSearch: (raw: Record<string, unknown>): SearchState => ({ query: typeof raw.query === "string" ? raw.query : "", page: Math.max(1, Number(raw.page ?? 1)), category: typeof raw.category === "string" ? raw.category : undefined, }), component: Catalog,});
function Catalog() { const search = Route.useSearch(); const navigate = Route.useNavigate();
return ( <input value={search.query} onChange={(event) => navigate({ search: (previous) => ({ ...previous, query: event.target.value, page: 1, }), replace: true, }) } /> );}Combine with Query Keys
Section titled “Combine with Query Keys”const search = Route.useSearch();
const productsQuery = useQuery({ queryKey: ["products", search], queryFn: () => fetchProducts(search),});If a URL value changes API results, it normally belongs in the Query key as well.
19. Route Organization
Section titled “19. Route Organization”A feature-oriented file-routed application may look like:
src/ routes/ __root.tsx index.tsx login.tsx _authenticated.tsx _authenticated/ dashboard.tsx products/ index.tsx $productId.tsx features/ auth/ useAuth.ts products/ ProductCard.tsx queries.ts router.tsx routeTree.gen.ts main.tsx| Guideline | Reason |
|---|---|
| Keep route modules focused on routing and page assembly | Feature code remains reusable |
| Keep query definitions near domain features | Routes and components can share them |
| Do not manually modify generated route tree files | Generation owns their contents |
| Prefer URL state for bookmarkable screen state | Back/forward/share behavior works naturally |
20. Common Mistakes and Fixes
Section titled “20. Common Mistakes and Fixes”| Mistake or Symptom | Cause | Fix |
|---|---|---|
| Navigation is not typed | Router type was not registered | Add the Register module declaration |
| Nested page never appears | Parent lacks <Outlet /> | Render an outlet in layout route |
| File route is omitted | Route is not exported as Route | Use export const Route = createFileRoute(...) |
| Dynamic link errors or loses safety | Param interpolated in to | Use to="/items/$id" plus params |
| Search page loses filters on refresh | Filters stored only in React state | Validate/store shareable state in URL search |
Loader calls useAuth() | Hooks cannot run in loader functions | Pass auth through router context |
| API data fetched twice in separate caches | Router and Query load independently | Have loader prime/query the Query cache |
| User sees blank screen during slow navigation | No pending UI | Supply route/global pending components |
| Editing generated tree causes churn | Generated output treated as source | Edit route files/configuration instead |
21. Copy-Paste Recipes
Section titled “21. Copy-Paste Recipes”File Route
Section titled “File Route”export const Route = createFileRoute("/about")({ component: () => <h1>About</h1>,});Param Route
Section titled “Param Route”export const Route = createFileRoute("/users/$userId")({ component: () => { const { userId } = Route.useParams(); return <h1>User {userId}</h1>; },});Typed Link
Section titled “Typed Link”<Link to="/users/$userId" params={{ userId: user.id }}> View user</Link>Loader
Section titled “Loader”export const Route = createFileRoute("/posts/$postId")({ loader: ({ params }) => fetchPost(params.postId), component: () => { const post = Route.useLoaderData(); return <h1>{post.title}</h1>; },});Auth Redirect
Section titled “Auth Redirect”beforeLoad: ({ context }) => { if (!context.auth.user) { throw redirect({ to: "/login" }); }}Query Loader
Section titled “Query Loader”loader: ({ context, params }) => context.queryClient.ensureQueryData(itemOptions(params.itemId))22. Learning Path and Quick Recall
Section titled “22. Learning Path and Quick Recall”Beginner
Section titled “Beginner”- Set up
__root.tsx, an index route,createRouter, andRouterProvider. - Navigate with typed
<Link>components. - Create nested layouts using
<Outlet />. - Read dynamic path parameters from
Route.useParams().
Intermediate
Section titled “Intermediate”- Validate search params and use them for bookmarkable UI filters.
- Add loaders with pending, error, and not-found views.
- Enable useful preload and route code splitting behavior.
- Use router context for authentication or service dependencies.
Advanced
Section titled “Advanced”- Protect route trees using
beforeLoadand authorization-aware designs. - Integrate TanStack Query for reusable shared server-state caching.
- Design route boundaries, URL state, loading feedback, and error handling together.
- Add SSR or framework-specific data lifecycles when the product needs them.
Recall Card
Section titled “Recall Card”Root route: createRootRoute({ component: () => <Outlet /> })File route: createFileRoute("/posts/$postId")({ component: Post })Router: createRouter({ routeTree, defaultPreload: "intent" })Render: <RouterProvider router={router} />Type safety: declare module "@tanstack/react-router" { interface Register { router: typeof router } }Link: <Link to="/posts/$postId" params={{ postId }} />Navigate: navigate({ to: "/login" })Params: const { postId } = Route.useParams()Search: const search = Route.useSearch()Loader data: const data = Route.useLoaderData()Auth guard: beforeLoad: () => { throw redirect({ to: "/login" }) }Query bridge: context.queryClient.ensureQueryData(options(id))