22 мар. 2025 г.·8 мин чтения

TanStack Router vs React Router для больших админок

TanStack Router против React Router для больших админок: сравните loaders, pending states и структуру маршрутов, пока кодовая база не превратилась в хаос.

TanStack Router vs React Router для больших админок

Почему навигация в админке быстро становится запутанной

Админки почти никогда не остаются маленькими. Сначала у команды есть дашборд, страница пользователей и настройки. Через несколько месяцев в том же приложении появляются биллинг, отчёты, роли, журналы аудита, согласования, экспорт и инструменты поддержки.

Навигация обычно становится запутанной раньше, чем это замечает кто-то из команды. Каждый новый раздел добавляет вложенные страницы, фильтры в URL, проверки прав, фоновые запросы и формы, которые должны сохранять данные, не ломая остальную часть экрана.

Общие layout'ы часто становятся первой настоящей проблемой. Родительский layout сначала выглядит аккуратно, потому что он оборачивает сайдбар, хедер и общие данные. Потом в него добавляют проверки авторизации, контекст аккаунта, feature flags, логику хлебных крошек, заголовки страниц, состояние модалок и запасной интерфейс. В итоге один файл начинает решать слишком много.

Первые тревожные признаки легко пропустить. Родительский маршрут загружает данные, которые нужны только одному дочернему экрану. Две соседние страницы по-разному обрабатывают редиректы или правила доступа. Небольшое изменение URL ломает вкладки, фильтры или поведение кнопки Назад в браузере. По отдельности ничего не выглядит катастрофой, но приложение начинает ощущаться хрупким.

Большие админки ещё и распыляют один экран по слишком многим файлам. Маршрут ведёт на страницу. Страница импортирует форму. Форма вызывает action. Loader получает данные где-то ещё. Guard живёт в обёртке. Обработка ошибок лежит на другом уровне. Каждый файл выглядит разумно, но весь поток запроса и рендера становится трудно проследить.

Именно поэтому выбор между TanStack Router и React Router важен в большой кодовой базе админки. Это не просто вопрос синтаксиса. Маршрутизация влияет на то, как команда думает о владении данными, pending UI и границах экранов. Если модель маршрутов кажется расплывчатой, люди чинят проблемы локально. Эти заплатки превращаются в привычки, а привычки — в негласные правила команды.

Биллинг — хороший пример. Добавьте счета, детали счёта, возвраты, настройки налогов и лимиты мест, и структура быстро станет глубокой. Если каждый экран придумывает свой собственный паттерн загрузки и проверки доступа, приложение всё ещё работает. Просто ежедневная работа становится медленнее. Новым разработчикам нужно больше времени, чтобы понять, где загружаются данные, где выполняются проверки прав и какой родительский маршрут управляет страницей.

Как каждый роутер моделирует приложение

На первый взгляд оба роутера похожи. Вы строите родительские маршруты, вкладываете под них дочерние страницы и переиспользуете layout'ы для сайдбара, верхней панели или оболочки аккаунта. Оба умеют работать с URL-параметрами и состоянием поиска, так что страница вроде /users/:userId?tab=security подходит для любого из них.

React Router сохраняет привычную ментальную модель. Команды обычно думают в терминах route objects и вложенных экранов, а потом добавляют loaders, actions и layout routes там, где это нужно. За счёт этого на старте работа идёт быстро. Небольшая админка с пользователями, биллингом и настройками часто остаётся простой для чтения.

TanStack Router с самого начала требует больше структуры. Он строит приложение вокруг типизированного дерева маршрутов, и это дерево задаёт навигацию, параметры и значения search params. Если команда меняет userId на memberId или переименовывает фильтр таблицы в URL, проверки типов находят несоответствие по всему приложению, а не оставляют его до запуска.

В этом и есть главное различие между ними. React Router даёт больше свободы. Многим командам это нравится. TanStack Router задаёт более жёсткие рельсы. И многим командам позже начинает хотеться именно этого.

Большая админка редко растёт аккуратно. Одна команда добавляет вложенные страницы настроек. Другая выносит фильтры таблиц в URL. Кто-то ещё добавляет маршруты для модалок, хлебные крошки, разделы по ролям и сохранённые представления. Оба роутера могут поддерживать всё это. Разница в том, насколько легко сохранить структуру одинаковой через шесть или восемь месяцев.

С React Router единообразие сильнее зависит от привычек команды. Два разработчика могут решить одну и ту же задачу по маршрутизации по-разному, и оба варианта будут работать. С TanStack Router дерево маршрутов чаще подталкивает людей к одному и тому же паттерну.

На первой неделе это может казаться слишком строгим. На восьмом месяце, когда в приложении уже 80 экранов и никто не хочет гадать, где живёт состояние маршрута, это часто ощущается спокойнее.

Загрузка данных в повседневной работе

Самая большая разница становится заметна, когда приложение вырастает больше чем до нескольких экранов. Вопрос уже не в том, "может ли этот маршрут загрузить данные?", а в том, "кто отвечает за запрос, когда он запускается и может ли следующая страница переиспользовать результаты?"

React Router размещает loaders в объектах маршрутов или модулях маршрутов. Они запускаются во время навигации, а совпавшие маршруты могут загружаться одновременно. TanStack Router тоже держит загрузку рядом с маршрутом, но предлагает более строгую модель вокруг route context, типизированных параметров и предварительной загрузки. На практике React Router сначала кажется проще. TanStack Router кажется более жёстким, и это часто помогает, когда админка становится большой.

Это особенно важно на вложенных страницах. Представьте /customers, /customers/:id и /customers/:id/billing. Родительский маршрут может один раз загрузить текущего пользователя, права доступа и выбранное рабочее пространство. Дочерние маршруты должны загружать только свои данные и не ждать, если они действительно не зависят от данных родителя. React Router тоже умеет делать это хорошо, но команды часто в итоге читают данные родительского loader'а через route matches и передают куски дальше. TanStack Router обычно делает такой поток родитель → ребёнок немного чище.

Для таблиц, форм и экранов деталей самый безопасный паттерн по-прежнему довольно скучный. Пусть маршрут списка загружает фильтры, пагинацию и первую страницу строк. Пусть маршрут редактирования загружает запись и справочные данные, которые нужны форме, например роли или статусы. Пусть маршрут деталей сначала загружает основную запись, а потом маленькие боковые панели обращаются к серверу сами, если они не блокируют всю страницу.

Скучный паттерн обычно побеждает, потому что позже его можно найти.

С TanStack Query

Если приложение уже использует TanStack Query, TanStack Router часто даёт более плавную связку. Loader маршрута может заполнить cache запросов, а компонент может прочитать те же данные без второго запроса. У вас остаётся один понятный источник правды для кеша, повторного запроса и устаревших данных.

React Router тоже может работать так же, но команде нужен чёткий правило и его нужно соблюдать. Если на одном экране возвращаются сырые данные loader'а, а на другом они же кладутся в query cache, кодовая база быстро расползается. Данные по-прежнему загружаются, но никто уже не понимает, где вообще должна жить логика запросов.

Для большинства команд loaders на маршруте должны отвечать за вход на страницу, проверки доступа и данные для первого рендера. Queries должны отвечать за повторное использование и фоновые обновления после этого.

Pending UI без мерцания

Пользователи замечают поведение загрузки сильнее, чем ожидают команды. В большой админке раздражает чаще не сама скорость, а рывок, когда таблица слишком рано очищается, форма исчезает до готовности следующей вкладки или модальный маршрут на долю секунды открывается пустой оболочкой.

TanStack Router обычно даёт более аккуратное место для управления этим на границе маршрута. Можно считать навигацию ответственностью маршрута, чуть дольше держать текущий экран видимым и показывать pending UI там, где переход действительно происходит. React Router тоже может делать то же самое, но команды часто смешивают состояние навигации с локальными спиннерами компонентов, и это быстро становится неровным.

Pending на уровне маршрута лучше всего работает, когда пользователь меняет место в приложении осмысленно: меняет страницу, вкладку, открывает детальную панель или модалку со своим URL. Одно правило загрузки на границе маршрута кажется спокойнее, чем пять отдельных спиннеров внутри карточек, таблиц и сайдбаров.

Локальные состояния загрузки всё ещё нужны для небольших действий внутри уже стабильного экрана. Фильтр, который обновляет одну таблицу, счётчик бейджа или второстепенный график, не обязаны заставлять весь маршрут выглядеть занятым. Если каждый маленький запрос включает route-wide pending state, приложение ощущается тяжёлым.

Хороший дефолт очень прост. Используйте route pending UI для реальных изменений навигации. Используйте локальный loading UI для обновления виджетов. Оставляйте старый контент видимым, пока новые данные почти готовы. Не очищайте списки и формы в тот же момент, когда запрос только начался.

Это особенно важно при медленных фильтрах, переключении вкладок и модальных маршрутах. Если пользователь меняет статус-фильтр, а таблица сразу исчезает, приложение кажется сломанным, даже если запрос завершается за 600 мс. Оставить предыдущие строки на экране, пусть даже слегка затемнёнными или временно заблокированными, намного лучше, чем заменять их пустым пространством.

Та же идея относится и к вкладкам. Если URL меняется с users на billing, оставьте старый контент вкладки видимым, пока новая вкладка не сможет показать что-то реальное. Для модальных маршрутов сразу показывайте рамку модалки, а тело загружайте внутри неё. Одно это решение убирает удивительно много визуального шума.

Если команде нужен один последовательный паттерн для большой разросшейся админки, TanStack Router часто проще держать в порядке. React Router тоже может подойти, но только если команда дисциплинированно разделяет, что относится к маршруту, а что — к компоненту.

Структура маршрутов до того, как расползутся файлы

Проверьте дерево маршрутов
Получите практичный разбор структуры маршрутов, пока мелкие несостыковки не расползлись по всей админке.

Большие админки обычно ломаются сначала на уровне маршрутов. Страницы по-прежнему работают, но дерево URL становится неаккуратным. Одна команда добавляет /users/:id, другая — /user-details/:id, а настройки оказываются разбросаны по трём разным layout'ам.

Простой map маршрутов сильно упрощает последующую уборку. Для админки полезно сначала набросать основные разделы, а уже потом создавать файлы: /app/dashboard, /app/users, /app/users/:userId, /app/settings/profile и /app/settings/billing уже отвечают на множество сложных вопросов. Дашборды находятся наверху. Страницы деталей лежат под своими списками. Вложенные настройки остаются сгруппированными, поэтому сайдбар, заголовок страницы и хлебные крошки сохраняют единый вид.

Проверки авторизации должны жить как можно выше в дереве. Публичные страницы вроде входа и сброса пароля должны оставаться вне защищённой оболочки приложения. Основной layout админки поместите в /app, а затем пусть дочерние маршруты наследуют сайдбар, хедер и общие проверки доступа. Если для billing settings нужны более жёсткие права, разместите это правило на ветке настроек, а не повторяйте его на каждой странице.

Предсказуемые названия становятся ещё важнее, когда с одной кодовой базой работают пять человек. Выберите один стиль и придерживайтесь его. Используйте множественное число для коллекций, например /users и /projects. Для страниц деталей используйте ID. Не смешивайте стили вроде /team-members, /staff и /users для одного и того же понятия. Такая путаница расползается в хлебные крошки, вкладки, тесты и конфигурацию меню.

Для search params тоже нужен план. Фильтры, сортировка, пагинация и диапазон дат обычно должны жить в URL, а не в скрытом состоянии. Если кто-то открыл список клиентов, отфильтрованный по "inactive", и отправил ссылку в поддержку, другой человек должен увидеть тот же экран.

С вкладками тоже лучше решить это заранее. Если вкладка меняет смысл страницы, сделайте её маршрутом вроде /users/:userId/activity. Если она меняет только маленькую панель, может хватить search param. Оба роутера справятся. Команды двигаются быстрее, когда определяют структуру до того, как папок станет слишком много.

Реалистичный пример админки

Представьте внутреннюю админку с тремя разделами: пользователи, счета и журналы аудита. Сотрудники весь день открывают список пользователей, заходят в одного пользователя, переключаются между вкладками вроде профиля, прав доступа и активности, а потом возвращаются назад в список, не теряя своё место.

На доске этот сценарий выглядит просто. Но стоит добавить фильтры, сохранённые search params, действия в строках, боковые панели и ещё десять экранов, как всё быстро усложняется.

На странице списка пользователей оба роутера могут загрузить данные до рендера экрана. React Router делает это через loaders, привязанные к маршрутам, и это хорошо работает для загрузки таблицы, текущих фильтров и состояния пагинации. TanStack Router решает ту же задачу, но часто ощущается более связанным с самим маршрутом, особенно когда search params играют большую роль.

Разница становится заметнее на странице пользователя. Допустим, у /users/42 есть вложенные вкладки для профиля, счетов и истории аудита. В TanStack Router родительский маршрут может один раз загрузить оболочку пользователя, а дочерние маршруты вкладок — только то, что нужно каждой вкладке. Дерево остаётся легче для чтения, потому что вложенность, параметры и состояние поиска живут в одной модели маршрутов.

React Router тоже может моделировать такой же экран, и многие команды делают это хорошо. Но в больших админках route objects нередко оказываются в одном месте, loaders — в другом, а правила данных для вкладок размазаны по нескольким файлам. Приложение всё ещё работает. Просто от команды оно требует больше.

Предзагрузка и возврат назад тоже меняют ощущение от приложения. Если человек наводит курсор на строку пользователя и затем открывает её, TanStack Router делает предзагрузку, основанную на намерении, более естественной, поэтому страница деталей часто появляется быстрее. Когда пользователь возвращается назад к списку, история маршрута и данных обычно остаётся проще для понимания.

С React Router тоже можно добиться плавного результата, но командам часто нужны дополнительные решения по кешированию и по тому, когда loaders должны запускаться снова. Без такой аккуратности возврат со страницы деталей к отфильтрованному списку может вызвать ещё один запрос и короткую вспышку в таблице.

После десяти дополнительных экранов выбор становится меньше про возможности и больше про поддержку. Для большой админки TanStack Router часто остаётся более читаемым, когда у вас глубокая вложенность, типизированные параметры и много правил предзагрузки. React Router тоже подходит, если команда уже хорошо его знает и с самого начала держит строгую структуру маршрутов.

Как выбирать шаг за шагом

Проверьте каркас приложения
Проверьте layout'ы, правила доступа и вложенные экраны, пока на них не завязалось ещё больше страниц.

Не оценивайте роутер по шести экранам, которые есть у вас сегодня. Оценивайте его по маршрутам, которые вам придётся поддерживать через год. Админки обычно растут скучно, но болезненно: больше вложенных настроек, больше фильтров таблиц, больше экранов деталей и больше страниц с правилами доступа.

Короткая проверка работает лучше, чем длинный спор:

  1. Набросайте карту маршрутов, которую вы ожидаете через год, включая страницы списков, страницы деталей, экраны редактирования, разделы настроек, отчёты и вложенные layout'ы.
  2. Отметьте страницы, которые сильно зависят от типизированных параметров, состояния поиска или и того и другого. Страница клиента с customerId, состоянием вкладки, фильтрами по датам и пагинацией — хороший тест.
  3. Возьмите один реальный сценарий и нарочно замедлите его. Откройте список, поменяйте фильтры, перейдите на страницу деталей, вернитесь назад и посмотрите на состояния загрузки.
  4. Проверьте, насколько легко поддерживать файлы маршрутов и правила именования в команде.

Этот третий шаг важнее, чем ожидают многие команды. Pending UI часто выглядит нормально на быстром ноутбуке и с локальными данными. А потом реальный пользователь открывает отчёт по слабому гостиничному Wi‑Fi, кликает дважды и видит устаревший контент, мерцание или пустой участок. TanStack Router часто ощущается сильнее там, где search params и pending states действительно влияют на повседневную работу. React Router часто кажется более знакомым, что важно, если команда уже хорошо использует его API для данных.

Обычно это вопрос трения, а не сырой мощности. Если у вашей админки глубокое состояние поиска, типизированные параметры маршрутов и много общих layout'ов, TanStack Router может предотвратить массу мелких ошибок. Если приложение пока среднего размера и команда уже мыслит в терминах React Router, React Router может остаться проще для обучения и ревью.

В финальном выборе должна решать последовательность. Роутер, который кажется удобным в руках одного инженера, может превратиться в хаос, когда пять человек добавляют маршруты по-разному. Выбирайте тот вариант, который команда будет использовать одинаково каждую неделю, даже когда дедлайны поджимают.

Если выбор всё ещё кажется близким, соберите один кусок админки обоими способами. Страница пользователей с фильтрами, пагинацией, формами редактирования и выдвижной панелью деталей расскажет вам больше за два часа, чем неделя обсуждений.

Ошибки, которые потом создают боль

Большие админки редко становятся запутанными только из-за роутера. Они запутываются, когда команды используют роутер по-разному, и никто не замечает этого, пока в приложении не появляется 80 экранов.

Одна команда кладёт проверки авторизации в файлы маршрутов. Другая — внутрь компонентов страниц. Третья обрабатывает права в layout-обёртке. Приложение по-прежнему работает, но никто не понимает, где именно живут правила маршрутов. Когда появляется новая функция, люди копируют то, что видели последним. Вот так маршрутизация и превращается в гадание.

С логикой запросов происходит то же самое. Если разработчики прячут загрузку данных внутри компонентов страниц, маршруты перестают честно рассказывать, что нужно экрану. Вы открываете файл маршрута и почти ничего не понимаете о том, какие данные нужны странице, когда она загружается и как выглядят ошибки. Позже команда пытается добавить предзагрузку, кеширование или более плавные переходы, и каждая страница ведёт себя немного по-своему.

Глобальные спиннеры — ещё одна ловушка. Сначала они кажутся простым решением, особенно в админских инструментах, где многие экраны загружают таблицы, фильтры и боковые панели. Но потом один маленький запрос блокирует всю страницу. Пользователи переключаются между записями и продолжают видеть один и тот же full-screen loader, который то появляется, то исчезает.

Более стабильный паттерн работает лучше: держите layout на месте, показывайте pending state только там, где меняются данные, и позволяйте остальной части экрана оставаться полезной.

Расхождение в названиях создаёт больше проблем, чем люди ожидают. В коде написано accounts, в меню — "Customers", в хлебных крошках — "Clients", а в аналитике — user-list. Никто не планирует такой беспорядок, но поддержка, дизайн и разработка начинают говорить на разных диалектах. Даже простая отладка занимает больше времени.

Несколько правил сильно снижают боль. Держите правила маршрутов в одном месте и не разбрасывайте их. Загружайте данные на уровне маршрута, когда экран зависит от них. Используйте локальные состояния загрузки, прежде чем тянуться к глобальному спиннеру. Выберите одну схему именования для URL, кода и видимых подписей.

Оба роутера могут поддерживать чистые админки. Проблемы начинаются тогда, когда команда воспринимает маршрутизацию как набор исключений, а не как общую систему.

Быстрые проверки перед тем, как принять решение

Приведите в порядок навигацию в админке
Устраните расползание маршрутов, смешанные проверки доступа и неровные паттерны загрузки с помощью опытного CTO.

Небольшие демо скрывают проблемы, которые вылезают через шесть месяцев. Большой админке нужен роутер, который делает рутинную работу очевидной, а не хитрой. Если новый человек в команде открывает кодовую базу и не может понять, откуда страница берёт данные, такая схема быстро устареет.

Помогает один простой тест. Возьмите экран с фильтрами, таблицей и панелью деталей и проследите его от маршрута до UI.

  • Может ли новый разработчик найти loader, search params и определение маршрута, не прыгая между пятью папками?
  • Может ли дизайнер сказать, что показывается во время загрузки: старый контент, skeleton, спиннер или частичное обновление?
  • Можно ли переместить страницу под новый родительский маршрут и сохранить helpers маршрутов, ссылки и типизированные параметры?
  • Может ли человек скопировать URL, обновить страницу, нажать Назад и сохранить те же фильтры и сортировку?

Эти проверки звучат скучно, но они рано ловят реальную боль. В админской работе search params часто так же важны, как и сама страница. Общий URL для "users, role = admin, sorted by last login" должен работать каждый раз. Если он ломается после обновления страницы или теряет состояние при возврате назад, за этим следуют обращения в поддержку.

Перемещения маршрутов — ещё один хороший тест. Команды постоянно переименовывают разделы. "Billing" становится "Finance", или "Settings" переезжает под "Operations". Если route helpers ломаются молча, уборка превращается в рискованный рефакторинг. Вам нужно, чтобы ошибки появлялись быстро, рядом с кодом маршрутов.

Для pending UI тоже нужен правило, которому могут следовать и дизайнеры, и разработчики. Держите каркас страницы стабильным, оставляйте фильтры видимыми и показывайте загрузку только внутри таблицы, если это согласованный паттерн. Такие решения уменьшают мерцание и помогают пользователям не терять ориентиры.

Если один роутер делает эти проверки проще именно в вашем приложении, это важнее, чем любые бенчмарки или вкусовые споры об API.

Что делать дальше

Не принимайте решение только по демо. Возьмите один кусок вашей админки, где уже есть настоящая сложность, например раздел пользователей с фильтрами, страницей деталей, формой редактирования и проверками прав. Соберите этот кусок целиком, прежде чем трогать остальную кодовую базу.

Этот небольшой эксперимент расскажет больше, чем неделя обсуждений. Вы увидите, насколько удобно читать loaders на практике, как ведут себя search params, когда экраны становятся загруженными, и кажутся ли pending states спокойными или раздражающими.

Пока вы строите, запишите несколько правил маршрутов: где живут loaders, как разбираются params и search params, когда UI оставляет старый экран вместо показа загрузки и как должны называться маршруты и папки, когда появится больше страниц. Короткий документ помогает команде не придумывать новый паттерн для каждой страницы.

Оцените результат после одной настоящей функции, а не после игрушечного примера. Возьмите фичу, где есть список, страница деталей, mutation и какой-то фоновый refresh. Потом задайте простые вопросы. Осталась ли логика загрузки лёгкой для чтения? Мерцал ли pending UI? Сохранило ли дерево маршрутов смысл после нескольких вложенных экранов?

Если один роутер выглядит чище в ежедневной работе, вот и ответ. Если оба кажутся нормальными, берите тот, которому команда будет следовать с меньшим количеством споров и без лишнего custom glue.

Если перед тем как зафиксировать структуру вам нужен ещё один взгляд со стороны, Oleg Sotnikov на oleg.is часто помогает командам с архитектурными решениями вроде границ маршрутов, потока данных и постепенных миграций в растущих продуктах. Такой разбор особенно полезен, когда у вас уже есть один рабочий кусок, потому что тогда обсуждение опирается на реальный код, а не на абстрактные предпочтения.