Skip to content

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 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.

ProblemRouter Tool
Display a component for a URLRoute
Render child screens inside a layout<Outlet />
Navigate without a full reload<Link /> / useNavigate()
Read /posts/123Path params
Store filters such as ?page=2Validated search params
Load before rendering a routeloader
Gate a private sectionbeforeLoad and redirect
Share services with loadersRouter context

Install the React router package:

Terminal window
npm install @tanstack/react-router

Optional development tools:

Terminal window
npm install @tanstack/react-router-devtools

TanStack Router supports two styles:

StyleDescriptionGood Starting Point
File-basedFiles under src/routes/ generate the route treeMost new applications
Code-basedBuild the route tree directly in TypeScriptSmall apps or custom configuration

The following sections begin with file-based routing, then show code-based equivalents later.


For file-based route generation in a Vite React app, install the plugin:

Terminal window
npm install -D @tanstack/router-plugin
vite.config.ts
import { 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.

src/
main.tsx
router.tsx
routeTree.gen.ts # Generated file; do not manually edit
routes/
__root.tsx
index.tsx
about.tsx

src/routes/__root.tsx
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 />
</>
);
}
src/routes/index.tsx
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/")({
component: HomePage,
});
function HomePage() {
return <h1>Home</h1>;
}
src/routes/about.tsx
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.


src/router.tsx
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;
}
}
src/main.tsx
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 PiecePurpose
routeTreeThe generated or manually built route hierarchy
createRouterCreates the router instance
RouterProviderRenders matches and supplies router state to React
Register declarationEnables 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.


Route directories and filenames express nested URLs and layouts.

src/routes/
__root.tsx
dashboard.tsx
dashboard/
index.tsx
settings.tsx
src/routes/dashboard.tsx
import { 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>
);
}
src/routes/dashboard/settings.tsx
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.


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>
);
}
<Link
to="/products/$productId"
params={{ productId: product.id }}
>
{product.name}
</Link>
Link PropPurpose
toDestination route pattern
paramsValues for path parameters
searchURL search state
hashFragment identifier
preloadWhether/how to preload route dependencies
activePropsProps used when this link is active

Pass dynamic values through params and search, rather than interpolating them into to.


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>;
}
import { redirect } from "@tanstack/react-router";
throw redirect({
to: "/login",
search: {
redirect: location.href,
},
});
UseTool
User clicks destination<Link>
Code navigates after an eventuseNavigate()
Loader/auth guard must leave the routethrow redirect(...)

A parameter segment begins with $.

src/routes/products/$productId.tsx
// src/routes/products/$productId.tsx
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/products/$productId")({
component: ProductPage,
});
function ProductPage() {
const { productId } = Route.useParams();
return <h1>Product: {productId}</h1>;
}
<Link
to="/products/$productId"
params={{ productId: "keyboard-101" }}
>
View keyboard
</Link>
URLRoute
/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.


Search parameters are URL state, such as filters and pagination. Validate them at the route boundary so components receive predictable types.

src/routes/products.tsx
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.

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 SearchUsually Does Not
Search query and filtersHover state
Sort order and table pageOpen state of a tiny temporary tooltip
Selected shareable tabUnsaved passwords
Values users can bookmark/shareLarge private draft objects

A route loader retrieves data required for that route. The router can start loading before the route component renders.

// src/routes/products/$productId.tsx
import { 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 OptionUse
loaderFetch route-required data
pendingComponentRender while slow route dependencies load
errorComponentDisplay errors in this route boundary
loaderDepsDeclare search-dependent loader inputs
staleTimeControl freshness for router-cached loader data
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.


TanStack Router includes route-loader caching. TanStack Query offers a broader shared server-state cache and mutation workflow.

SituationGood Fit
Route needs data only while visiting that routeRouter loader cache
Data shared across multiple routes/componentsTanStack Query
Mutations, invalidation, optimistic UI neededTanStack Query
Straightforward page preload with little shared API dataRouter 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.


Provide the Query Client in Router Context

Section titled “Provide the Query Client in Router Context”
src/routes/__root.tsx
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 />,
});
src/router.tsx
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;
}
}
src/main.tsx
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>,
);
src/features/products/queries.ts
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.tsx
import { 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.


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 ContextAvoid
Authentication state needed for guardsCalling React hooks inside loaders
Query client or API clientRandom component-only UI state
Feature flags affecting accessHiding all dependencies in global imports

Use beforeLoad for access checks that must happen before child routes load.

src/routes/_authenticated.tsx
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.tsx

Authentication determines whether a user is signed in; authorization must still check whether that user may perform an operation, especially on the server.


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>
),
});
StateProvide
Route/code/data loadingPending feedback or skeleton
Failed loadError boundary with useful recovery
Unknown URL/resourceNot found experience
Protected routeRedirect or permission feedback

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-BasedCode-Based
Route tree generated from route modulesRoute tree assembled by code
Less manual tree wiringMaximum explicit control
Common choice for growing appsUseful for small/custom route systems

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,
});
TechniqueBenefit
Intent preloadingData/code begins loading on likely navigation
Route code splittingInitial JavaScript bundle can remain smaller
Loader + Query prefetchDestination data may already be cached

Preload routes users are likely to visit; very aggressive preloading can waste network and device resources.


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,
})
}
/>
);
}
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.


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
GuidelineReason
Keep route modules focused on routing and page assemblyFeature code remains reusable
Keep query definitions near domain featuresRoutes and components can share them
Do not manually modify generated route tree filesGeneration owns their contents
Prefer URL state for bookmarkable screen stateBack/forward/share behavior works naturally

Mistake or SymptomCauseFix
Navigation is not typedRouter type was not registeredAdd the Register module declaration
Nested page never appearsParent lacks <Outlet />Render an outlet in layout route
File route is omittedRoute is not exported as RouteUse export const Route = createFileRoute(...)
Dynamic link errors or loses safetyParam interpolated in toUse to="/items/$id" plus params
Search page loses filters on refreshFilters stored only in React stateValidate/store shareable state in URL search
Loader calls useAuth()Hooks cannot run in loader functionsPass auth through router context
API data fetched twice in separate cachesRouter and Query load independentlyHave loader prime/query the Query cache
User sees blank screen during slow navigationNo pending UISupply route/global pending components
Editing generated tree causes churnGenerated output treated as sourceEdit route files/configuration instead

export const Route = createFileRoute("/about")({
component: () => <h1>About</h1>,
});
export const Route = createFileRoute("/users/$userId")({
component: () => {
const { userId } = Route.useParams();
return <h1>User {userId}</h1>;
},
});
<Link to="/users/$userId" params={{ userId: user.id }}>
View user
</Link>
export const Route = createFileRoute("/posts/$postId")({
loader: ({ params }) => fetchPost(params.postId),
component: () => {
const post = Route.useLoaderData();
return <h1>{post.title}</h1>;
},
});
beforeLoad: ({ context }) => {
if (!context.auth.user) {
throw redirect({ to: "/login" });
}
}
loader: ({ context, params }) =>
context.queryClient.ensureQueryData(itemOptions(params.itemId))

  1. Set up __root.tsx, an index route, createRouter, and RouterProvider.
  2. Navigate with typed <Link> components.
  3. Create nested layouts using <Outlet />.
  4. Read dynamic path parameters from Route.useParams().
  1. Validate search params and use them for bookmarkable UI filters.
  2. Add loaders with pending, error, and not-found views.
  3. Enable useful preload and route code splitting behavior.
  4. Use router context for authentication or service dependencies.
  1. Protect route trees using beforeLoad and authorization-aware designs.
  2. Integrate TanStack Query for reusable shared server-state caching.
  3. Design route boundaries, URL state, loading feedback, and error handling together.
  4. Add SSR or framework-specific data lifecycles when the product needs them.
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))