Доки по разработке
This project is maintained by teniryte
createRootRoute(options) — единственный корневой узел; задает глобальный layout, errorBoundary, pendingBoundary и context. В v1 принято держать rootRoute в отдельном файле (__root.tsx), чтобы избежать циклических зависимостей.createRoute(options) — базовый дочерний узел с обязательным getParentRoute.createLazyRoute(options) — ленивый аналог; позволяет вернуть Component, loader, pendingComponent из dynamic import, сокращая initial bundle.createFileRoute(path) / createLazyFileRoute(path) — синтаксический сахар для file-based подхода. Автоматически проставляют id вида /dashboard/reports.id ('dashboard-layout'), чтобы потом к ним легко привязывать guards/children.routes/dashboard/index.ts экспортирует dashboardRoute и dashboardChildren, а корневой routeTree собирается в routes/__root.tsx.routeTree отдельно от router, чтобы подменять context во время unit-тестирования.| Опция | Назначение |
| — | — |
| path | Статический ('settings') или динамический ('$userId') сегмент. Разрешены вложенные сегменты ('$orgId/projects'). |
| id | Пользовательский идентификатор, обязателен, если нет path. Удобен для layout-узлов. |
| component / pendingComponent / errorComponent | Компоненты для состояний. Можно задавать функцией или ссылкой на компонент. |
| loader | Загрузка данных; поддерживает Promise, defer, работу с Query Client. |
| beforeLoad | Выполняется до монтирования; подходит для guard-логики и вычисления context. |
| shouldReload | Управляет повторными вызовами loader-а; можно возвращать false, если данные кешируются Query Client. |
| meta | Свободная полезная нагрузка (title, breadcrumbs, featureFlags). |
| validateSearch / parseParams | Типобезопасная валидация search/params через zod/valibot. |
| pendingMs | Таймаут до показа pendingComponent; снижает “мигание” спиннера. |
| wrapInSuspense | Принудительно оборачивает вложенные компоненты в Suspense (полезно при createLazyRoute). |
$: '$userId/settings'. Для optional сегментов используйте '$userId?'.id и mask, но старайтесь избегать, чтобы не усложнять devtools.createFileRoute('/dashboard/$orgId') автоматически ставит path: '$orgId' и id: '/dashboard/$orgId'.// routes/dashboard.tsx
export const Route = createRoute({
getParentRoute: () => rootRoute,
id: 'dashboard',
component: () => (
<DashboardShell>
<Outlet />
</DashboardShell>
),
meta: () => ({ title: 'Панель' }),
beforeLoad: ({ context }) => {
if (!context.auth.user) {
throw redirect({ to: '/login', mask: { to: '/' } })
}
},
})
// routes/dashboard.analytics.tsx
export const Route = createRoute({
getParentRoute: () => dashboardRoute,
path: 'analytics',
component: AnalyticsPage,
loader: async ({ context, search }) =>
context.api.analytics.get({ range: search.range ?? '7d' }),
validateSearch: z.object({
range: z.enum(['7d', '30d']).default('7d'),
}),
})
createRoute({
getParentRoute: () => dashboardRoute,
path: 'reports',
pendingMs: 150,
pendingComponent: () => <Spinner />,
errorComponent: ({ error }) => <ErrorPanel error={error} />,
loader: async ({ context }) => context.api.reports.list(),
component: ReportsPage,
})
// routes/dashboard.reports.lazy.tsx
export const Route = createLazyRoute({
component: async () => {
const { ReportsLayout } = await import('./ReportsLayout')
return { Component: ReportsLayout }
},
pendingComponent: () => <SkeletonReports />,
})
// routes/dashboard.reports.$reportId.tsx
export const Route = createFileRoute('/dashboard/reports/$reportId')({
loader: async ({ params, context }) =>
context.queryClient.ensureQueryData(reportQuery(params.reportId)),
component: ReportDetails,
})
contextcontext передается из createRouter({ context }) и доступен во всех loader/beforeLoad.
const router = createRouter({
routeTree,
context: {
auth: authClient,
queryClient,
featureFlags,
},
})
QueryClient, trpcClient), а не UI-состояния.request, но на клиенте заменяйте его сериализуемой версией (например, requestHeaders).context через Route.context. Например, dashboardRoute может запровайдить org всем дочерним матчам.const authRouteFactory = rootRoute.createRoute({
beforeLoad: ({ context }) => {
if (!context.auth.user) throw redirect({ to: '/login' })
},
})
export const settingsRoute = authRouteFactory.createRoute({
path: 'settings',
component: SettingsPage,
})
const orgRouteFactory = authRouteFactory.createRoute({
parseParams: (params) => ({
orgId: z.string().uuid().parse(params.orgId),
}),
})
export const orgMembersRoute = orgRouteFactory.createRoute({
path: '$orgId/members',
loader: ({ params, context }) =>
context.api.org(params.orgId).members(),
})
component получает RouteComponentProps<TFullRoute>; используйте Route.useLoaderData() и Route.useSearch() для типобезопасности.lazy подключений удобно писать const Route = createLazyFileRoute('/path')({ component: lazy(...) }).router.navigate/router.invalidate после fetch. Можно совмещать с RouterViewTransition для плавных обновлений.Route.useRouteContext() вместо React context, чтобы гарантировать подписку только на нужный матч.component через RouteContext.Provider или Route.context.useMatch({ select: (match) => match.loaderData.part }).memo, если внутри много состояния.useRouterState({ select }), чтобы подписываться на нужные поля (например, status, location).routerOptions.dehydrate/hydrate только при SSR; иначе React будет тратить время на сериализацию.loader просто await queryClient.ensureQueryData(...)).createLazyRoute хорошо сочетается с router.loadRoute(to). Вызывайте его в onMouseEnter, чтобы подгружать bundle заранее.usePrefetchRoute (v1.0+) позволяет подгрузить loader/компонент при пересечении sentinel-а (IntersectionObserver).pendingMs, чтобы короткие переходы не показывали спиннер.const ReportsLink = () => {
const router = useRouter()
return (
<Link
to="/dashboard/reports"
onMouseEnter={() => router.loadRoute('/dashboard/reports')}
>
Отчеты
</Link>
)
}
loaderDeps помогает привязать loader к значениям из search, params или контекста.router.invalidate({ to: '/dashboard/reports' }) — это перезапустит только связанные loader-ы.const reportsRoute = dashboardRoute.createRoute({
path: 'reports',
loaderDeps: ({ search }) => search.status ?? 'all',
loader: ({ loaderDeps, context }) =>
context.api.reports.list({ status: loaderDeps }),
})
Link и router.navigate поддерживают params, search, hash, replace, viewTransition и mask.{ to: '/dashboard', search: true }) позволяет показать один layout во время загрузки другого (похоже на Remix).router.navigate принимает throwOnError. Можно ловить ошибки и показывать toast вместо перехода.await router.navigate({
to: '/dashboard/reports/$reportId',
params: { reportId },
search: (prev) => ({ ...prev, focus: 'chart' }),
mask: { to: '/dashboard', search: true },
replace: true,
})
@tanstack/router-devtools есть вкладка Tree — отлично показывает, какие layout-уровни активны, и где pending.getParentRoute вернет undefined).routes/index.ts, где явно перечислены все дочерние маршруты и экспортирован routeTree.process.env.NODE_ENV === 'development' && <TanStackRouterDevtools router={router} />.beforeLoad у layout-узлов).pendingMs.createRouter, layout-ы расширяют его при необходимости.Route.useLoaderData).loadRoute, usePrefetchRoute) настроен для дорогих веток.