Skip to content

Установка и настройка

sh
npm i reactive-route
sh
yarn add reactive-route
sh
pnpm add reactive-route

Reactive Route — это npm-пакет без каких-либо зависимостей с отдельными импортами модулей для бесшовного подключения к имеющейся системе реактивности на любом фреймворке (React, Preact, Solid, Vue). Настраивать tree-shaking не требуется.

Peer dependencies

Если используется не "коробочная" реактивность фреймворка, должны быть установлены подходящие реактивные библиотеки (см. Интеграции).

Карта модулей
ts
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:

ts
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 }) для исключения циклических импортов

Передача конфигурации объектом более типобезопасна
ts
"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 вызывать отдельную функцию:

tsx
"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.

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

tsx
"mobx-router": "1.0.0"

import { Home } from './Home';

export const routes = {
  home: new Route({ path: '/', component: <Home /> })
}
tsx
"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.

tsx
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);
}
tsx
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);
}
tsx
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);
}
ts
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` обязательно
tsx
"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 браузера и отобразить компонент страницы.

tsx
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>
);
tsx
import { Router } from 'reactive-route/react';

import { useRouter } from './router';

export function App() {
  const { router } = useRouter();

  return <Router router={router} />;
}
tsx
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')!
);
tsx
import { Router } from 'reactive-route/preact';

import { useRouter } from './router';

export function App() {
  const { router } = useRouter();

  return <Router router={router} />;
}
tsx
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')!
);
tsx
import { Router } from 'reactive-route/solid';

import { useRouter } from './router';

export function App() {
  const { router } = useRouter();

  return <Router router={router} />;
}
ts
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');
vue
<script lang="ts" setup>
  import { Router } from 'reactive-route/vue';

  import { useRouter } from './router';

  const { router } = useRouter();
</script>

<template>
  <Router :router="router" />
</template>

No AI participated in the development. MIT License