Ограничения
Хотя большинство библиотек для роутинга предпочитает копировать весь функционал, который есть у конкурентов, и жестко привязывать к своей экосистеме, Reactive Route сохраняет фокус на:
- реактивном хранении данных о текущей странице в
router.state[name] - асинхронной подготовке к открытию следующей страницы
- удобном интерфейсе для редиректов (т.к. нативный
window.pushState('/new-path?phone=1234')не типизирован, выполняется синхронно и не работает на сервере или в виджетах) - независимости от фреймворка и системы реактивности
Позитивным трендом сейчас является TypeScript, поэтому весь функционал, не совместимый со статической типизацией, отсутствует. Сюда входят опциональные параметры в пути, JSX-объявление <Route path="untyped[?-partial]-string/:id/:id/:id">, файловый роутинг posts/$postId/$/$.tsx и другие практики, разрушающие типобезопасность и структуру.
Nested routes / Динамические роуты
Представьте такую валидную с точки зрения ряда альтернативных роутеров конфигурацию, но которой никогда не будет в Reactive Route:
const configs = createConfigs({
user: {
path: '/user/:id',
params: { id: validators.numeric },
// imagine we have a nested route here
children: {
default: {
// :id collision with parent
path: 'default/:id',
// validator schema collision with parent
params: { id: validators.notNumeric },
loader: () => import('./pages/user'),
},
// name collision with parent
user: {
// :id collision with parent
path: 'view/:id',
// validator schema collision with parent
params: { id: validators.alphaNumeric },
loader: () => import('./pages/user/view'),
}
}
},
});
// try to extend the configs
configs.extend({
parent: 'user.user',
children: {
default: {
// path collision with parent path: 'view/:id'
path: 'default/:id',
// validator schema collision with parent
params: { id: validators.notNumeric },
// component collision with parent
loader: () => import('./pages/user'),
}
}
}Коллизии имен и валидаторов нельзя решить на уровне статического анализа, а редиректы становятся сложными и ненадежными, без ясного жизненного цикла.
В роутерах, использующих подобные паттерны, приходится в рантайме решать коллизии, держать в голове всю структуру компонентов и их динамики, а рефакторинг требует полного переделывания логики редиректов по частичным строкам путей без помощи TypeScript.
Также крайне сложно сделать стабильный поток загрузки данных и проверки прав пользователя. Будет ли вызван beforeEnter второго уровня при изменении params или query третьего уровня, и наоборот?
Разумеется, страдает и DX: отсутствует поддержка "Find Usages" или быстрой навигации в IDE, ограниченная поддержка автодополнения, отсутствуют подсказки при описании редиректов, а при использовании ИИ при рефакторинге требуется передавать весь код проекта, так как структура роутов строится в рантайме.
Таким образом, код выше сразу становится legacy и требует экспертных знаний о проекте, кардинально усложняя развитие кодовой базы, параллельную работу команды и прозрачность.
Hash / History State
В Reactive Route не поддерживаются URL hash и History State, и принудительно очищаются при редиректах. Их использование усложняет проектирование и делает состояние приложения фрагментированным.
В библиотеке есть два мощных механизма для динамических параметров — опциональные query и обязательные params. Они типизированы, участвуют в жизненных циклах, валидируются и способны эффективно решать задачи, для которых традиционно использовался hash.
Также Reactive Route не привязан к History API, что позволяет использовать его для встраиваемых виджетов или микрофронтендов с полноценным асинхронным роутингом, изолированным от других частей приложения, причем на любом фреймворке.
Нестроковые params и query
Так как браузерный URL содержит только строковые значения, в Reactive Route нет утилит для автоматической конвертации в разные типы данных.
const configs = createConfigs({
user: {
path: '/user/:id',
loader: () => import('./pages/user'),
// validation is required and stable
params: { id: validators.numeric },
query: { phone: validators.numeric.length(6, 15) },
},
});
// 100% safe casts
const { params, query } = router.state.user!;
const id = computed(() => Number(params.id));
const phone = computed(() => query.phone ? Number(query.phone) : null);
// URL can't have numbers, only strings.
// No hidden magic of type conversion
router.redirect({
name: 'user',
params: { id: '9999' },
query: { phone: '123456' }
})В этом примере строковые значения проверяются с помощью validators.numeric, который либо специфичен для проекта, либо берется из любой из сотен библиотек для валидации. Подразумевается, что он уже проверил значение на NaN, Infinite, -0 и подтвердил, что строка при Number(params.id) является корректным числом.
Но само приведение к Number / Boolean / Object / Array не встроено в библиотеку и как в примере выше является ответственностью разработчика. Это позволяет использовать структуры любой сложности через собственные механизмы десериализации.
Ряд библиотек имеет встроенные утилиты для приведения значений к определенному типу, однако это создает иллюзию, что URL может хранить и обрабатывать не только строки. Подходы к валидации становятся некорректными, что особенно заметно при попытке возложить на роутер автоматическое приведение к Object / Array / Date и парсинг сложных структур.
Нетипизированные beforeEnter / beforeLeave
TypeScript 5 пока не умеет рекурсивно выводить типы для beforeEnter и beforeLeave, поэтому аргументы currentState, nextState и redirect имеют упрощенные типы. Описывать логику в них нужно осторожно — при рефакторинге TS не подсветит ошибок.
Это ограничение касается только жизненного цикла, во всех остальных сценариях сохраняется полная и строгая типизация. На этот компромисс пришлось пойти, так как выделение функций жизненного цикла за пределы createConfigs приводит к расползанию логики работы со страницами и ухудшает DX.