Доки по разработке
This project is maintained by teniryte
После создания router объявите модульное расширение:
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
Так Link, useNavigate, useRouterState будут знать про params, search, context.
Тонкости:
router. IDE быстрее подхватывают *.d.ts, если файл импортируется хотя бы раз.types/router.d.ts и добавляйте его в tsconfig.references, чтобы не было дублирования Register.@tanstack/router, иначе Register['router'] превратится в never.type RouteComponentProps<T extends AnyRoute> = {
route: T
useLoaderData: () => LoaderData<T>
}
export function PostPage({
useLoaderData,
}: RouteComponentProps<typeof Route>) {
const post = useLoaderData()
}
Лайфхаки:
RouteById<AppRouter, 'posts/$postId'> для layout-компонентов — так вы получите тип params и loaderData сразу.withRoute(RouteComponent) который принимает AnyRoute и возвращает компонент с пропом route. TypeScript сохранит конкретные поля staticData, context.useLoaderData указывайте typeof Route.loader через Awaited<ReturnType<typeof Route.loader>>, чтобы не дублировать структуры данных.createRootRouteWithContextinterface RouterContext {
queryClient: QueryClient
session: Session | null
}
export const rootRoute = createRootRouteWithContext<RouterContext>()({
beforeLoad: ({ context }) => {
invariant(context.session, 'Auth required')
},
})
createRootRouteWithContext гарантирует, что дочерние маршруты увидят context.context в createRouter({ context }). В тестах подставляйте фейковый объект; TS проследит за обязательными полями.satisfies RootRouteOptions<RouterContext> к объекту опций, чтобы IDE подсказывала доступные свойства (beforeLoad, notFoundComponent, meta).parseParams отдавайте брендовые типы (type TenantId = Brand<string,'TenantId'>), чтобы не перепутать userId и orgId.validateSearch используйте zod/valibot + satisfies RouteValidateSearchOptions, чтобы не потерять сигнатуру.Пример:
const searchSchema = z.object({
page: z.coerce.number().int().positive().default(1),
tags: z.array(z.string()).catch([]),
})
export const postsRoute = createFileRoute('/posts')({
validateSearch: searchSchema.parse,
loaderDeps: ({ search }) => ({ page: search.page }),
loader: async ({ deps }) => fetchPosts(deps.page),
})
useSearch({ from: postsRoute.id }) подскажет оба поля, а navigate({ to: postsRoute.to, search: { page: '1' } }) подсветится как ошибка.z.preprocess → undefined, иначе Router решит, что поле обязательно.export const routes = router.buildRouteTree()
routes.blog.post.$postId({ postId: 1 }).to
buildRouteTree() создает объект с типизированными to, fullPath, params, useLoaderData, useStaticData.
Полезные приёмы:
routes.org.$orgId.useParams() — автодополнение параметров, удобно для shared-хуков (useCurrentOrg(routes.org.$orgId)).routes.toArray() пригодится в тестах: можно пробежаться по всем маршрутам и убедиться, что staticData содержит title.route.mask({ params, search }) помогает строить ссылки без ручных шаблонов.export const useRouteParams = <T extends AnyRoute>(route: T) => route.useParams()
export function useTypedNavigate<TRoute extends AnyRoute>(route: TRoute) {
const navigate = useNavigate()
return (opts?: Parameters<typeof route.navigate>[0]) =>
navigate({ ...opts, to: route.to })
}
useRouteSearch, useRouteStaticData, useRouteLoaderData — редактор сохранит типы за счёт generic.useMemo(() => route.useLoaderData(), [route.id]), чтобы не потерять неизменяемость (TS понимает зависимости).lazyRouteComponent(() => import('./page')) satisfies RouteComponent.type Module = Awaited<typeof import('./page')> и используйте Module['loader'].lazyRouteComponent(() => import('./page').then((m) => ({ default: m.Page }))) — тип RouteComponent сохранится.lazyRoute + ValidateSearch → TS проверит схему ещё до загрузки файла.Link принимает generic RouteIds: Link<RouteIds, { search: Search }> запрещает неправильные параметры.useMatchRoute:
const matchRoute = useMatchRoute()
const match = matchRoute({ to: routes.dashboard.to, search: { tab: 'stats' } })
if (match) {
console.log(match.params) // типизированы
}
router.preloadRoute({ to: route.to, params }) — параметры выводятся из route.type Guard = (opts: RouteGuardOptions<AppRouter>) => Promise<void> и применять через beforeLoad."moduleResolution": "bundler" (Vite) или "nodenext" (SSR) помогут корректно резолвить модульные пути Router."types": ["@tanstack/react-router"], если TS не видит расширение.@typescript-eslint/consistent-type-imports, кастомное tanstack-router/no-raw-path (запрещает to="/foo"), tanstack-router/require-validate-search."verbatimModuleSyntax": true, чтобы не было дубликатов import Route.src/lib/router.ts:
export const router = createRouter({ routeTree, context })
export type AppRouter = typeof router
export type RoutePaths = keyof AppRouter['flatRoutes']
export type RouteIds = AppRouter['routeTree']['id']
export type RouterRoute<T extends RouteIds> = RouterRouteById<AppRouter, T>
AppRouter и используйте в DI-контейнере (container.register(AppRouterToken, router)).RouterRoute<'posts/$postId'> помогает создать фиктивные данные без обращения к реальному Router.await router.load() с memoryHistory для unit-тестов loaders.renderWithRouter(<Link ... />, { router }).context и dehydrateRouter(router) для e2e.expect(router.state.matches).toMatchInlineSnapshot() фиксирует search, status, context.loader так:
const loader = (opts: LoaderFnOpts) => fetchData(opts)
const route = createFileRoute('/x')({
loader: loader satisfies LoaderFn<LoaderFnOpts, LoaderResult>,
})
type NavigationMeta = RouterState['pendingMatches'][number]['meta'] — пригодится для метрик загрузки.beforeLoad возвращайте Promise<void> и логируйте context.logger.debug({ routeId }) — типы routeId и params будут проверены.traceId в context, а в loader описывайте loader: ((opts) => instrument(opts, realLoader)) satisfies LoaderFn.routes.* или RouteById.search/params локализуйте в одном helper (легче рефакторить).satisfies RouteOptions.type LoaderData = Awaited<ReturnType<typeof loader>>.beforeLoad и action описывайте через satisfies, чтобы не потерять context.select в route.useLoaderData({ select }), чтобы типы соответствовали выбранному полю.TenantId, AccountId и возвращайте их в parseParams.expectType<RouteById<'known/id'>>() в тестах — IDE гарантирует, что ID существует.createRootRouteWithContext и прокинут в createRouter.RouteComponentProps, RouteById или маршрутовые хуки.params/search валидируются и возвращают брендовые типы.routes.*, useTypedNavigate заменяют строковые пути.satisfies, loaders/actions проверяются.router.load, renderWithRouter, snapshot router.state.meta.