Изображение, созданное DALL-E
Изображение, созданное DALL-E

Привет, Хабр!
Если вы разрабатываете приложения на React, вы наверняка сталкивались с вопросом, как организовать маршрутизацию. И хотя инструменты вроде React Router или Next.js Router уже давно стали де-факто стандартом, на рынке появился новый игрок - TanStack Router. Он позиционируется как “современный, масштабируемый и полностью типобезопасный роутер”, вобравший лучшие практики из Next.js, Remix/React Router и других популярных решений. Давайте разберёмся, что это за зверь и чем он может быть полезен.

Ключевые особенности

Полная типобезопасность

Не секрет, что библиотеки, написанные на TypeScript или имеющие type definitions, ещё не гарантируют нам 100% покрытие типов. TanStack Router, напротив, спроектирован с учётом TS на каждом шаге.

IDE подскажет вам, если вы, например, неправильно указали имя параметра в пути ($postId вместо $postID) или забыли передать обязательные параметры.

Встроенная валидация путей и query-параметров: не только парсинг, но и проверка типов - всё это доступно из коробки.

Встроенный механизм загрузки данных (Loaders) с кэшированием

На практике роутер зачастую ассоциируется лишь с переключением компонентов при изменении URL. Но в современных SPA всё более важен вопрос data fetching (избегание водопадов запросов, автоматическая подгрузка, кэширование и т.п.). TanStack Router предлагает:

  • API для загрузки данных (loader) перед рендером. Это уменьшает риск водопадов, так как вы можете параллельно грузить сразу несколько наборов данных в разных роутерах.

  • Stale-while-revalidate кэш из коробки (или можно подключить свой, например, TanStack Query).

  • Автоматический префетч данных при наведении на ссылку (hover) или иных предзагрузочных ситуациях, чтобы страницы открывались быстрее.

Мощные API для работы с search-параметрами

Query-параметры в браузерном URL - зачастую недооценённый механизм. TanStack Router даёт целый инструментарий для их типобезопасного хранения, обновления и валидации:

  • JSON-сериализация и парсинг - можно сохранять сложные объекты, а не только простые ?page=2.

  • Валидация - если ожидается, что page - число, роутер проверит это автоматически.

  • Вложенные роуты автоматически наследуют search-схемы родительских роутов, так что дочерние видят все родительские параметры.

  • Прослойки (search.middlewares) позволяют централизованно изменять search-параметры (например, удалять дефолтные значения или сохранять нужные поля) перед записью в URL.

  • Удобные хуки и компоненты

    • useSearch/Route.useSearch() - читает (и при желании пишет) валидированные параметры из текущего URL, будто это обычное React-состояние.

    • useNavigate / <Navigate> / router.navigate() - позволяют менять search-параметры программно, без лишних плясок с URLSearchParams.

    • <Link search={...}> - задаёт или модифицирует query-параметры прямо в декларативном стиле.

SSR и апгрейд до фреймворка

Хотя TanStack Router сам по себе - это только клиентский роутер, он создан с прицелом на SSR, стриминг и другие возможности.

  • Разработчики уже ведут работу над TanStack Start - полноценным фреймворком, основанным на Router + Vite + Nitro + Vinxi.

  • Это потенциально может стать интересной альтернативой Next.js/Remix, если вам нужна глубокая типизация и гибкость.

Code splitting и lazy loading

Библиотека умеет разделять код роутов на критический (парсинг пути, валидация, loader) и некритический (компоненты, ошибки, pending-состояния).

  • Можно вручную делить файлы на .tsx и .lazy.tsx.

  • При использовании file-based routing доступен автоматический code-splitting, когда рендерящие компоненты загружаются по требованию.

Devtools

Отладка роутинга - один из болевых пунктов при создании сложных SPA. TanStack Router предлагает специальные Devtools, где можно посмотреть структуру роутов, загрузку данных, состояние кэша. Поддержка пока только в React, но в будущем могут появиться и другие адаптеры.

Сравнение с популярными решениями

TanStack Router vs. React Router

Хотя React Router (и Remix на его базе) - самый распространённый вариант в экосистеме React, у TanStack Router есть ряд преимуществ:

Сравниваемый аспект

TanStack Router

React Router

Типобезопасность

Полная типобезопасность из коробки. IDE сразу подскажет, если неверно указать имя параметра или тип search-параметров.

Не предоставляет строгую типизацию параметров и query-параметров “из коробки”. Базовые типы есть, но для глубокой валидации URL-параметров на уровне TS придётся либо писать логику самостоятельно, либо обращаться к дополнительным инструментам.

Кэширование и SWR

Встроенные loader'ы с поддержкой кэширования и SWR-подхода (либо интеграция с TanStack Query, SWR, Apollo и т.д.).

Исторически не решает задачу асинхронных лоадеров и кэширования из коробки. Remix улучшил это, но Remix - отдельный фреймворк со своими правилами.

Работа с search-параметрами

Полноценный стейт-менеджмент (JSON-сериализация, валидация, обновление через хуки и Link). Оптимистичные переходы, глубокая типизация query-параметров.

Всё сводится к URLSearchParams, без дополнительной валидации/типизации.

Структура роутов (File-Based Routing)

Можно комбинировать file-based и code-based routing. File-based Routing генерирует дерево на основе структуры файлов автоматически.

По умолчанию - code-based <Routes> и <Route>. File-based Routing требует Remix или сторонних решений.

Devtools

Есть официальные Devtools: позволяют одним кликом увидеть состояние роутов, кэша, ошибок, лоадеров.

Нет официального devtools-плагина. Можно воспользоваться сonsole.log или React DevTools, но это не так удобно.

Контекст и рендер-пайплайны

Имеются гибкие хуки beforeLoad, onLoad и route context, упрощающие SSR/SSG, авторизацию, middleware.

Для подобных задач нужны дополнительные инструменты или библиотеки.

Размер бандла

~80.2 kB Minified; ~24.4 kB Minified + Gzipped.

~151 kB Minified; ~46.3 kB, Minified + Gzipped.

Таким образом, если вы любите строгую типизацию, хотите встроенное кэширование и удобную работу с query-параметрами, а также цените меньший вес бандла, TanStack Router даёт эти плюшки “из коробки”, без необходимости городить самописные решения.

TanStack Router vs. Next.js

В Next.js тоже есть роутер (и file-based routing), но в целом Next.js - это большой фреймворк со встроенным SSR/SSG, API routes и другими абстракциями. Если вы не используете Next.js как полный фреймворк, а вам нужен только клиентский роутер, TanStack Router может стать более лёгким и гибким решением.

  • Next.js фокусируется на SSR/SSG и монолитном подходе, со своими соглашениями, что иногда бывает избыточно или сложно при интеграции в старые SPA-проекты.

  • TanStack Router - это чистый роутер без чужих надстроек: можно подключить SSR и прочие вещи по вкусу, а не вынужденно.

Подробное сравнение можно посмотреть тут.

Пример базовой конфигурации (code-based routing)

import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import {
  createRouter,
  createRootRoute,
  createRoute,
  RouterProvider,
  Outlet,
  Link,
} from '@tanstack/react-router'

import { TanStackRouterDevtools } from '@tanstack/router-devtools'

// Создаём корневой роут
const rootRoute = createRootRoute({
  component: () => (
    <>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
      </nav>
      <Outlet />
      <TanStackRouterDevtools />
    </>
  ),
})

// Создаём дочерние роуты
const indexRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/',
  component: () => <div>Главная страница</div>,
})

const aboutRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/about',
  component: () => <div>О нас</div>,
})

// Собираем дерево роутов
const routeTree = rootRoute.addChildren([indexRoute, aboutRoute])

// Создаём экземпляр роутера
const router = createRouter({ routeTree })

// Регистрируем типы (чтобы роутер работал с TS без костылей)
declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

// Точка входа в приложение
ReactDOM.createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <RouterProvider router={router} />
  </StrictMode>
)

Так мы получаем простой роутер с двумя маршрутами (/ и /about). При переходах автоматически рендерятся соответствующие компоненты.

File-Based Routing

Если хочется почувствовать “волшебство” и не вручную описывать все пути, можно использовать file-based routing. Тогда структура /src/routes/… сама задаёт иерархию роутов, а специальный плагин генерирует дерево автоматически.

Пример структуры файлов:

src/
 ┣ routes/
 ┃  ┣ __root.tsx   # корневой роут
 ┃  ┣ index.tsx    # / 
 ┃  ┗ about.tsx    # /about
 ┣ main.tsx
 ┗ routeTree.gen.ts (автоматически сгенерирован)

В __root.tsx можно задать общий Layout, хэдэры/футеры, Devtools и прочие глобальные вещи.

Загрузка данных (Loaders)

TanStack Router “из коробки” предлагает loader, который позволяет загрузить данные до рендеринга компонента и при этом закэшировать их.

// src/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
import { fetchPosts } from '@/api'

export const Route = createFileRoute('/posts')({
  // Лоадер, который получает список постов
  loader: async () => fetchPosts(),
  component: Posts,
})

function Posts() {
  // Достаём данные из лоадера
  const posts = Route.useLoaderData()
  return (
    <ul>
      {posts?.map((p) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  )
}

Теперь если мы переходим на /posts, то перед рендером компонента будут загружены (и закэшированы) данные. А если мы снова вернёмся на /posts в течение времени жизни кэша, новый запрос не понадобится.

С живым примером можно поиграться тут

Поиск и валидация search-параметров

Использование ?searchQuery=... - отличный способ хранить состояние интерфейса (фильтры, пагинация и т.д.). В TanStack Router можно указать валидацию параметров, а затем читать и менять их без ручной возни с URLSearchParams.

Ниже пример маршрута с валидацией и компонента, который точечно обновляет параметры с помощью useNavigate:

// Создаём маршрут с валидацией:
const ProductsRoute = createFileRoute('/products')({
  validateSearch: (search) => ({ ...search, filter: search.filter ?? '' }),
  component: Products,
})

// Компонент, который читает и изменяет query-параметры:
function Products() {
  const { filter } = ProductsRoute.useSearch()
  const navigate = useNavigate({ from: ProductsRoute.fullPath })

  function setActiveFilter() {
    navigate({ search: (old) => ({ ...old, filter: 'active' }) })
  }

  return (
    <div>
      <p>Текущий фильтр: {filter}</p>
      <button onClick={setActiveFilter}>Активные товары</button>
    </div>
  )
}

Так filter хранится в URL, а не только в локальном состоянии, что упрощает шаринг, перезагрузку и совместную работу.

Оптимизация рендеринга (Render Optimizations)

TanStack Router включает несколько механизмов оптимизации, чтобы ваши компоненты не перерисовывались понапрасну. Основные из них:

Структурное расшаривание (Structural Sharing)
Когда вы храните данные, например в search-параметрах, и меняется лишь часть объекта (только одно поле), TanStack Router пытается сохранить неизменившиеся части как те же ссылки. Это похоже на работу иммутабельных структур из Redux или React Query. Например, если у вас есть foo и bar в поисковых параметрах, и вы меняете только bar, то ссылка на foo останется прежней, что уменьшит перерисовки.

Тонкая подписка (Fine-grained selectors)
Вы можете подписаться только на нужные поля состояния (например, лишь на конкретное поле search-параметров). Делается это с помощью select в хуках. Например:

// Этот компонент перерендерится, только если изменится foo
const foo = Route.useSearch({ select: ({ foo }) => foo })

Если при навигации меняется bar, а foo остаётся неизменным, ваш компонент не перерисуется.

Structural Sharing + Select
Если вы возвращаете из select объект, есть риск, что при каждом рендере будет создаваться новая ссылка (и, значит, компонент заново перерисуется). Для этого можно включить структурное расшаривание, чтобы TanStack Router сравнивал структуру и переиспользовал объекты, которые не изменились.

Включается глобально через defaultStructuralSharing: true при создании роутера:

const router = createRouter({
  routeTree,
  defaultStructuralSharing: true,
})

Или точечно, в отдельных хук-вызовах:

const result = Route.useSearch({
  select: (search) => {
    return {
      foo: search.foo,
      hello: `Hello, ${search.foo}`,
    }
  },
  structuralSharing: true,
})

При этом сами данные должны быть JSON-совместимыми (примитивы, plain-объекты, массивы). Если вы вернёте в select, скажем, new Date(), компилятор подскажет, что structural sharing для классовых объектов не поддерживается.

Devtools

Чтобы понимать, как роутер выбирает маршруты, где и как загружаются данные, предусмотрены TanStack Router Devtools. Они показывают текущее дерево роутов, состояние кэша, асинхронные загрузки и ошибки. Пример подключения:

  import { TanStackRouterDevtools } from '@tanstack/router-devtools'
  
  function App() {
    return (
      <>
        <RouterProvider router={router} />
        <TanStackRouterDevtools router={router} />
      </>
    )
  }

Панелька при желании открывается/закрывается по клику, и её состояние (открыта/закрыта) запоминается в localStorage.

Дополнительные возможности

Ниже кратко перечислим ещё несколько полезных фич, которые могут понадобиться в продвинутых кейсах (все они детально описаны в официальной документации TanStack Router):

  • Virtual File Routes. Если обычный file-based routing не покрывает все нужды, вы можете гибко формировать дерево роутов программно - указывать реальные файлы или целые директории, которые подключаются под нужный путь.

    • Например, часть приложения работает по виртуальным роутам, а другая - по классической файловой структуре.

    • Полезно, когда нужно объединить несколько подходов или подмонтировать стандартную папку с роутами в произвольное место URL.

  • SSR и стриминг. TanStack Router умеет работать на сервере как в классическом non-streaming SSR, так и в стриминге (Streaming SSR).

    • Non-streaming: вы загружаете нужные данные, формируете HTML, а на клиенте гидрируете уже полностью готовую страницу.

    • Streaming SSR: можно по частям отдавать критически важный контент (быстрый рендер) и параллельно дотягивать тяжёлые данные, улучшая перцептивную скорость.

  • Route Masking. Позволяет показывать пользователю один URL, а физически вести роутер по другому пути. Иногда удобно, если часть URL не должна быть видна в адресной строке (например, параметры модального окна или служебные query).

  • Authenticated Routes. В beforeLoad можно проверять аутентификацию пользователя и либо пускать его к нужному контенту, либо редиректить на логин. Также TanStack Router легко интегрируется с React-контекстами/хранилищами, которые отвечают за авторизацию.

  • Data Mutations. Мутации часто удобнее обрабатывать вне роутера (например, через TanStack Query или SWR). Но можно комбинировать их и с кэшем TanStack Router, используя router.invalidate() для пере-fetch’а после успешной мутации.

  • Scroll Restoration. TanStack Router умеет восстанавливать позицию скролла при навигации - как для всей страницы, так и для отдельных скролл-контейнеров. Это упрощает жизнь разработчику, ведь не надо городить самописные решения для сохранения/восстановления scroll position.

  • Гибкое управление ошибками. Кроме классических boundary-компонентов (errorComponent, notFoundComponent), есть возможность бросать notFound() из лоадера или компонента, и TanStack Router передаст управление ближайшему “Not Found” обработчику.

  • External Data Loading. Если встроенного кэша недостаточно, роутер отлично сочетается с TanStack Query или другими библиотеками (RTK Query, SWR, Relay). Можно прокидывать эти клиенты через контекст роутера и использовать loader, чтобы префетчить нужные данные до рендера.

Кому может подойти TanStack Router?

  • Командам, которые любят TypeScript и хотят максимально использовать возможности типобезопасности (автодополнение, защита от неверной навигации).

  • Проектам, где важно оптимизировать загрузку данных, избегать повторных запросов к API и упростить работу с query-параметрами.

  • Фанатам TanStack Query - Router отлично дополняет Query, создавая единый удобный стэк для клиентской части.

  • Тем, кто хочет гибкости - нужен SSR, стриминг, частичный file-based routing или замаскированные URL - всё это можно прикрутить.

Если вы уже используете React Router или Next.js Router - переход, возможно, потребует времени, особенно если проект крупный. Но для новых проектов TanStack Router точно заслуживает внимания.

Итоги

TanStack Router - это свежая кровь в сфере роутинга для React. Он сочетает глубокую типобезопасность, продвинутые механизмы загрузки и кэширования данных, удобные инструменты для работы с search-параметрами, а также оптимизации рендеринга и множество плюшек вроде Devtools, SSR, маршрутизации по файлам или виртуальной. Хотя экосистема вокруг него ещё не столь обширна, как у классических решений, у него большие амбиции (вспомним про TanStack Start).

Если для вас важны строгий TypeScript-контроль и гибкие возможности работы с данными (suspense-подходы, автоматический SWR-кэш, Devtools и т.д.), TanStack Router заслуживает пристального внимания. Возможно, именно он станет тем компонентом, которого вам не хватало в очередном реакт-проекте.

Спасибо, что дочитали до конца! Если вы уже пробовали TanStack Router или у вас есть вопросы, делитесь в комментариях. Удачной разработки!

Комментарии (1)


  1. miss_polly
    06.01.2025 13:22

    Какая прелесть! Понравилась типобезопасность и кэшированием из коробки. Вы пробовали его на реальном проекте? ну или может кто еще :)