Dev Highlights

Доки по разработке

This project is maintained by teniryte

Зачем нужны Routing Trees

В TanStack Router v1 маршруты описываются как дерево. Каждый узел определяет:

Такой подход даёт:

  1. Типобезопасные ссылки: параметры пути и поиска выводятся из дерева.
  2. Статический анализ для code splitting, SSR, prefetch и дев-обновлений.
  3. Общий контекст между ветками (layout делится session, queryClient, theme и т. д.).
  4. Автоматический linkBuilder с автодополнением и подсказками IDE.
  5. Возможность подключать DevTools и визуально видеть, какие узлы загружены/ожидают.

Лайфхак: если видите странные ошибки типов в ссылках, просто переимпортируйте routeTree — генератор прогоняет весь типовой вывод и подсвечивает конфликты путей ещё до runtime.

Базовый пример дерева

// src/routes/routeTree.gen.ts (генерируется плагином)
import { Route as rootRoute } from './__root'
import { Route as indexRoute } from './index'
import { Route as blogRoute } from './blog'
import { Route as blogPostRoute } from './blog.$postId'

export const routeTree = rootRoute.addChildren([
  indexRoute,
  blogRoute.addChildren([blogPostRoute]),
])

Когда собирать дерево вручную

Структура layout-веток

// routes/blog.tsx
import { Outlet, createRoute } from '@tanstack/react-router'
import { Route as rootRoute } from './__root'

export const Route = createRoute({
  getParentRoute: () => rootRoute,
  path: 'blog',
  beforeLoad: ({ context }) => {
    if (!context.session) {
      throw new Redirect({ to: '/login', search: { next: '/blog' } })
    }
    return { theme: context.theme ?? 'light' }
  },
  component: () => (
    <BlogLayout>
      <Suspense fallback={<BlogSkeleton />}>
        <Outlet />
      </Suspense>
    </BlogLayout>
  ),
})

Лайфхаки:

Индексные маршруты

export const Route = createRoute({
  getParentRoute: () => blogRoute,
  path: '/',
  component: () => <PostsList />,
})

Индексы не имеют собственного сегмента URL, поэтому:

Динамические сегменты

export const Route = createRoute({
  getParentRoute: () => blogRoute,
  path: '$postId',
  parseParams: (raw) => ({
    postId: Number(raw.postId),
  }),
  stringifyParams: ({ postId }) => ({
    postId: postId.toString(),
  }),
  component: PostView,
})

Советы:

Вычисляемые пути и search-схемы

import { z } from 'zod'

export const Route = createRoute({
  getParentRoute: () => blogRoute,
  path: '$category/$slug',
  validateSearch: z.object({
    draft: z.boolean().default(false),
    cursor: z.string().optional(),
  }),
  loader: async ({ params, context }) => {
    const queryKey = ['post', params.category, params.slug]
    await context.queryClient.ensureQueryData({
      queryKey,
      queryFn: () => fetchPost(queryKey),
    })
    return { queryKey }
  },
})

Лайфхак: используйте router.navigate({ search: prev => ({ ...prev, cursor: nextCursor }) }) — тогда типы search и мемоизация гарантированы.

Плагины генерации дерева

@tanstack/router-plugin собирает дерево из файлов по шаблону routes/**/*.tsx.

Правила:

  1. Каждый файл экспортирует Route.
  2. Имя файла = путь (blog.$postId.lazy.tsxblog/$postId).
  3. Файл __root.tsx обязателен (глобальные провайдеры, error boundaries).

Полезные мелочи:

Советы по проектированию дерева

Route Context и шаринг состояния

export const Route = createRoute({
  getParentRoute: () => blogRoute,
  path: '$postId',
  beforeLoad: async ({ context, params }) => {
    const post = await context.api.getPost(params.postId)
    return { post }
  },
  component: () => {
    const { post } = Route.useRouteContext()
    return <PostView post={post} />
  },
})

Интеграция с React Query

export const Route = createRoute({
  getParentRoute: () => blogRoute,
  path: '$postId',
  loader: ({ context, params }) =>
    context.queryClient.ensureQueryData({
      queryKey: ['post', params.postId],
      queryFn: () => fetchPost(params.postId),
    }),
  component: () => {
    const { params } = Route.useRoute()
    const { data } = useQuery(['post', params.postId], { enabled: false })
    return <PostView post={data} />
  },
})

Lazy-маршруты и code splitting

Типобезопасные ссылки и навигация

router.navigate({
  to: blogPostRoute.to,
  params: { postId: 1 },
  search: { highlight: 'comments' },
  mask: { to: '/blog', search: true }, // user видит маску
  replace: false,
})

DevTools и диагностика

Типичные подводные камни

Чек-лист

Дополнительные ресурсы