Skip to content
Dashboard

Build type-safe React routes with TanStack Router

Link to headingFile routes and code routes

src/routes/projects.$projectId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/projects/$projectId')({
component: ProjectPage,
loader: async ({ params }) => {
return fetchProject(params.projectId)
},
})
function ProjectPage() {
const project = Route.useLoaderData()
return <h1>{project.name}</h1>
}

import {
createRootRoute,
createRoute,
createRouter,
Outlet,
} from '@tanstack/react-router'
const rootRoute = createRootRoute({
component: () => <Outlet />,
})
const projectsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/projects/$projectId',
component: ProjectPage,
})
const routeTree = rootRoute.addChildren([projectsRoute])
export const router = createRouter({ routeTree })

Link to headingType-safe route params

export const Route = createFileRoute('/teams/$teamId/reports/$reportId')({
loader: async ({ params }) => {
return getReport({
teamId: params.teamId,
reportId: params.reportId,
})
},
component: ReportPage,
})
function ReportPage() {
const { teamId, reportId } = Route.useParams()
const report = Route.useLoaderData()
return <ReportView teamId={teamId} reportId={reportId} report={report} />
}

Link to headingSearch params become app state

Link to headingHow TanStack Router models search params

type IssueSearch = {
page: number
status: 'open' | 'closed'
assignee: string | undefined
}
export const Route = createFileRoute('/issues')({
validateSearch: (search): IssueSearch => {
let page = 1
if (typeof search.page === 'string') {
page = Number(search.page)
}
let status: IssueSearch['status'] = 'open'
if (search.status === 'closed') {
status = 'closed'
}
let assignee: string | undefined
if (typeof search.assignee === 'string') {
assignee = search.assignee
}
return {
page,
status,
assignee,
}
},
loaderDeps: ({ search }) => ({
page: search.page,
status: search.status,
assignee: search.assignee,
}),
loader: async ({ deps }) => {
return fetchIssues(deps)
},
})

Link to headingLoaders belong to routes

export const Route = createFileRoute('/runs/$runId')({
loader: async ({ params, context, abortController }) => {
return context.api.getRun({
runId: params.runId,
signal: abortController.signal,
})
},
component: RunPage,
})
function RunPage() {
const run = Route.useLoaderData()
return <RunTimeline run={run} />
}

import { createRootRouteWithContext } from '@tanstack/react-router'
type RouterContext = {
api: {
getRun(input: { runId: string; signal: AbortSignal }): Promise<Run>
}
}
const rootRoute = createRootRouteWithContext<RouterContext>()({
component: AppShell,
})

Link to headingPending UI stays local

import { useRouter } from '@tanstack/react-router'
export const Route = createFileRoute('/reports/$reportId')({
loader: async ({ params }) => getReport(params.reportId),
pendingComponent: ReportSkeleton,
errorComponent: ({ error }) => {
const router = useRouter()
return <ReportError error={error} onRetry={() => router.invalidate()} />
},
component: ReportPage,
})

Link to headingLayouts define route boundaries

src/routes/_app.tsx
export const Route = createFileRoute('/_app')({
component: AppLayout,
})
function AppLayout() {
return (
<Shell>
<Outlet />
</Shell>
)
}

Link to headingProtecting nested routes

import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ context, location }) => {
const session = await context.auth.getSession()
if (!session) {
throw redirect({
to: '/login',
search: {
redirect: location.href,
},
})
}
return { session }
},
component: AuthenticatedLayout,
})

Link to headingTanStack Start completes the stack

Link to headingVercel runs every React stack

Link to headingTanStack Router FAQ

Ready to deploy?