Установка и настройка
npm i reactive-routeyarn add reactive-routepnpm add reactive-routeReactive Route — это npm-пакет без каких-либо зависимостей с отдельными импортами модулей для бесшовного подключения к имеющейся системе реактивности на любом фреймворке (React, Preact, Solid, Vue). Настраивать tree-shaking не требуется.
Peer dependencies
Если используется не "коробочная" реактивность фреймворка, должны быть установлены подходящие реактивные библиотеки (см. Интеграции).
Карта модулей
import { createConfigs, createRouter } from 'reactive-route';
import { Router } from 'reactive-route/solid';
import { Router } from 'reactive-route/react';
import { Router } from 'reactive-route/preact';
import { Router } from 'reactive-route/vue';
import { adapters } from 'reactive-route/adapters/mobx-react';
import { adapters } from 'reactive-route/adapters/mobx-preact';
import { adapters } from 'reactive-route/adapters/mobx-solid';
import { adapters } from 'reactive-route/adapters/solid';
import { adapters } from 'reactive-route/adapters/kr-observable-react';
import { adapters } from 'reactive-route/adapters/kr-observable-preact';
import { adapters } from 'reactive-route/adapters/kr-observable-solid';
import { adapters } from 'reactive-route/adapters/vue'Создание конфигурации
В терминологии Reactive Route описание роута / маршрута и его поведения называется Config и передается в createConfigs:
import { createConfigs, createRouter } from 'reactive-route';
import { adapters } from 'reactive-route/adapters/{reactive-system}';
const configs = createConfigs({
home: {
path: '/',
loader: () => import('./pages/home'),
}
});
export const router = createRouter({ configs, adapters });
// somewhere
await router.redirect({ name: 'home' });loader ожидает, что компонент страницы будет в экспорте default.
При необходимости можно объявлять path с переменными /:id/:name, в этом случае для каждой переменной необходим валидатор. Никогда не доверяйте пришедшим в URL данным, особенно если есть SSR.
Не рекомендуется
сразу передавать компонент loader: () => Promise.resolve({ default: HomePage }) для исключения циклических импортов
Передача конфигурации объектом более типобезопасна
"vue-router": "5.0.0"
const routes = [
{ name: 'home', path: '/', component: Home }
]
"@kitbag/router": "0.22.0"
const routes = [
createRoute({ name: 'home', path: '/', component: Home })
]В библиотеках роутинга нередко конфигурация передается массивом, что создает возможность коллизии name, а TypeScript не может обеспечить их уникальность.
Reactive Route автоматически добавляет name в Config, равный ключу в объекте, таким образом обеспечивая уникальность ключей, именованные роуты и отсутствие коллизий еще до запуска кода.
Также в библиотеке минимизировано количество бойлерплейта — не нужно на каждый Config вызывать отдельную функцию:
"mobx-router": "1.0.0"
const configs = {
home: new Route({ path: '/', component: <Home /> })
}
"@tanstack/react-router": "1.157.15"
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: Home,
})
const routeTree = rootRoute.addChildren([indexRoute])Использование асинхронного импорта расширяет возможности
Асинхронные импорты поддерживаются всеми современными бандлерами, позволяя разделять код на чанки (code-splitting).
Также это мощный инструмент для модульных архитектур, когда при импорте приходит не только default: Component, но и постраничные сторы / апи / другие модульные сервисы, и Reactive Route позволяет с ними взаимодействовать.
Файлы с чанками загружаются оптимизированно — только когда прошли все проверки и жизненный цикл, а при необходимости можно их предзагрузить вручную через router.preloadComponent.
Также исключается возможность циклических импортов, которые могут нарушить работу приложения:
"mobx-router": "1.0.0"
import { Home } from './Home';
export const routes = {
home: new Route({ path: '/', component: <Home /> })
}"mobx-router": "1.0.0"
import { routes } from './routes';
export function Home() {
const store = useContext(StoreContext);
return <Link router={store.router} route={routes.home} />
});Экспорт
Рекомендуется использовать Context API для передачи роутера в компоненты.
Но вы можете сами выбрать схему
Экспорт с помощью Singleton-паттерна (как в предыдущем примере) проще и подходит для CSR-проектов.
В SSR же несколько пользователей могут одновременно зайти на разные страницы, и Singlton отрендерит html-разметку из последнего текущего состояния, а не нужного конкретному пользователю, поэтому требуется изоляция в виде контекстов или другого DI.
import { createContext, useContext } from 'react';
import { createConfigs, createRouter } from 'reactive-route';
import { adapters } from 'reactive-route/adapters/{reactive-system}';
export function getRouter() {
const configs = createConfigs({
home: {
path: '/',
loader: () => import('./pages/home'),
},
user: {
path: '/user/:id',
params: {
id: (value) => /^\d+$/.test(value)
},
query: {
phone: (value) => value.length < 15
},
loader: () => import('./pages/user'),
},
notFound: {
path: '/not-found',
props: { errorCode: 404 },
loader: () => import('./pages/error'),
},
internalError: {
path: '/internal-error',
props: { errorCode: 500 },
loader: () => import('./pages/error'),
}
});
return createRouter({ adapters, configs });
}
export const RouterContext = createContext<{
router: ReturnType<typeof getRouter>
}>(undefined);
export function useRouter() {
return useContext(RouterContext);
}import { createContext } from 'preact';
import { useContext } from 'preact/hooks';
import { createConfigs, createRouter } from 'reactive-route';
import { adapters } from 'reactive-route/adapters/{reactive-system}';
export function getRouter() {
const configs = createConfigs({
home: {
path: '/',
loader: () => import('./pages/home'),
},
user: {
path: '/user/:id',
params: {
id: (value) => /^\d+$/.test(value)
},
query: {
phone: (value) => value.length < 15
},
loader: () => import('./pages/user'),
},
notFound: {
path: '/not-found',
props: { errorCode: 404 },
loader: () => import('./pages/error'),
},
internalError: {
path: '/internal-error',
props: { errorCode: 500 },
loader: () => import('./pages/error'),
}
});
return createRouter({ adapters, configs });
}
export const RouterContext = createContext<{
router: ReturnType<typeof getRouter>
}>(undefined);
export function useRouter() {
return useContext(RouterContext);
}import { createContext, useContext } from 'solid-js';
import { createConfigs, createRouter } from 'reactive-route';
import { adapters } from 'reactive-route/adapters/{reactive-system}';
export function getRouter() {
const configs = createConfigs({
home: {
path: '/',
loader: () => import('./pages/home'),
},
user: {
path: '/user/:id',
params: {
id: (value) => /^\d+$/.test(value)
},
query: {
phone: (value) => value.length < 15
},
loader: () => import('./pages/user'),
},
notFound: {
path: '/not-found',
props: { errorCode: 404 },
loader: () => import('./pages/error'),
},
internalError: {
path: '/internal-error',
props: { errorCode: 500 },
loader: () => import('./pages/error'),
}
});
return createRouter({ adapters, configs });
}
export const RouterContext = createContext<{
router: ReturnType<typeof getRouter>
}>(undefined);
export function useRouter() {
return useContext(RouterContext);
}import { InjectionKey, inject } from 'vue';
import { createConfigs, createRouter } from 'reactive-route';
import { adapters } from 'reactive-route/adapters/{reactive-system}';
export function getRouter() {
const configs = createConfigs({
home: {
path: '/',
loader: () => import('./pages/home'),
},
user: {
path: '/user/:id',
params: {
id: (value) => /^\d+$/.test(value)
},
query: {
phone: (value) => value.length < 15
},
loader: () => import('./pages/user'),
},
notFound: {
path: '/not-found',
props: { errorCode: 404 },
loader: () => import('./pages/error'),
},
internalError: {
path: '/internal-error',
props: { errorCode: 500 },
loader: () => import('./pages/error'),
}
});
return createRouter({ adapters, configs });
}
export const routerStoreKey: InjectionKey<{
router: ReturnType<typeof getRouter>
}> = Symbol();
export function useRouter() {
return inject(routerStoreKey)!;
}Наличие `notFound` и `internalError` обязательно
"vue-router": "5.0.0"
const routes = [
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }
]
"mobx-router": "1.0.0"
startRouter(
configs,
store,
{ notfound: () => store.router.goTo(YOUR_NOT_FOUND_ROUTE), }
);
"@kitbag/router": "0.22.0"
export const router = createRouter(routes, {
rejections: [
createRejection({ type: 'NotFound', component: NotFoundPage })
]
})В библиотеках для роутинга синтаксис определения страниц ошибок зачастую сильно отличается от "обычных страниц", а их наличие является опциональным, что в ряде случаев приведет к отображению пустой страницы.
В Reactive Route они обязательны для обеспечения стабильности, но разрешено определять только path, loader и props. На них можно переходить страндартно router.redirect({ name: 'notFound' }).
internalError будет отображена без замены URL в истории браузера при необработанных исключениях в жизненном цикле или ошибке загрузки чанка страницы. Reactive Route здесь является "последней линией обороны" и обеспечивает дополнительную консистентность приложению.
Запуск
Роутер готов к работе, осталось найти первоначальный Config, который соответствует URL браузера и отобразить компонент страницы.
import { createRoot } from 'react-dom/client';
import { App } from './App';
import { getRouter, RouterContext } from './router';
const router = getRouter();
await router.init(location.href);
createRoot(document.getElementById('app')!).render(
<RouterContext.Provider value={{ router }}>
<App />
</RouterContext.Provider>
);import { Router } from 'reactive-route/react';
import { useRouter } from './router';
export function App() {
const { router } = useRouter();
return <Router router={router} />;
}import { render } from 'preact';
import { App } from './App';
import { getRouter, RouterContext } from './router';
const router = getRouter();
await router.init(location.href);
render(
<RouterContext.Provider value={{ router }}>
<App />
</RouterContext.Provider>,
document.getElementById('app')!
);import { Router } from 'reactive-route/preact';
import { useRouter } from './router';
export function App() {
const { router } = useRouter();
return <Router router={router} />;
}import { render } from 'solid-js/web';
import { App } from './App';
import { getRouter, RouterContext } from './router';
const router = getRouter();
await router.init(location.href);
render(
() => (
<RouterContext.Provider value={{ router }}>
<App />
</RouterContext.Provider>
),
document.getElementById('app')!
);import { Router } from 'reactive-route/solid';
import { useRouter } from './router';
export function App() {
const { router } = useRouter();
return <Router router={router} />;
}import { createApp } from 'vue';
import App from './App.vue';
import { getRouter, routerStoreKey } from './router';
const router = getRouter();
await router.init(location.href);
createApp(App, { router })
.provide(routerStoreKey, { router })
.mount('#app');<script lang="ts" setup>
import { Router } from 'reactive-route/vue';
import { useRouter } from './router';
const { router } = useRouter();
</script>
<template>
<Router :router="router" />
</template>