Skip to content

API

Config

Содержит всю логику работы со страницей

Настраиваемые свойства
ts
path: string
Путь маршрута, должен начинаться с / и может включать динамические сегменты
ts
props?: Record<string, any>
Статичные props, передаваемые в компонент страницы
ts
params?: Record<
  TypeExtractParams<TPath>,
  (value: string) => boolean
>
Валидаторы для динамических сегментов path
ts
query?: Record<
  string,
  (value: string) => boolean
>
Валидаторы для query параметров
ts
loader: () => Promise<{
  default: PageComponent,
  ...otherExports
}>
Функция, возвращающая Promise с компонентом в параметре default
ts
beforeEnter?: (data: {
  reason: TypeReason;
  nextState: TypeStateUntyped;
  currentState?: TypeStateUntyped;
  redirect: (
    stateDynamic: 
      TypeStateDynamicUntyped & 
      { replace?: boolean; }
  ) => void;
}) => Promise<void | (
  TypeStateDynamicUntyped & 
  { replace?: boolean; }
)>;
Функция жизненного цикла, вызываемая перед редиректом на страницу
ts
beforeLeave?: (data: {
  reason: TypeReason;
  nextState: TypeStateUntyped;
  currentState: TypeStateUntyped;
  preventRedirect: () => void;
}) => Promise<void>;
Функция жизненного цикла, вызываемая перед уходом со страницы
Системные свойства
ts
name: string

Соответствует ключу объекта

ts
component?: any
Поле default, возвращенное loader
ts
otherExports?: Record<string, any>

Все экспорты, возвращенные loader, кроме default

config.params

В Reactive Route нет явного разделения статичных и динамических Config, просто сегменты пути с префиксом : контролируются валидаторами, разрешающими открывать страницу со значением из URL, и это отражается в типизации.

ts
users: {
  path: '/users',  // no validators
  loader: () => import('./pages/users')
},
userDetails: {
  path: '/user/:id',  // "id" is a param matched by the validator
  params: {
    id: (value) => /^\d+$/.test(value)
  },
  loader: () => import('./pages/userDetails')
}
Тесты типизации Config
ts
import { createConfigs } from '../../packages/core';

const loader = async () => ({ default: null });

const v = () => true;

createConfigs({
  dynamic: { path: '/:id/:tab', params: { id: v, tab: v }, loader },
  static: { path: '/', loader },
  notFound: { path: '/404', props: {}, loader },
  internalError: { path: '/500', props: {}, loader },

  // @ts-expect-error "params" must be "never"
  static1: { path: '/', loader, params: {} },

  // @ts-expect-error "params" are required
  dynamic1: { path: '/:id/:tab', loader },
  // @ts-expect-error "params.tab" and "params.id" are required
  dynamic2: { path: '/:id/:tab', params: {}, loader },
  // @ts-expect-error "params.tab" is required
  dynamic3: { path: '/:id/:tab', params: { id: v }, loader },
  // @ts-expect-error "params" validators must be functions
  dynamic4: { path: '/:id/:tab', params: { id: v, tab: '' }, loader },

  // @ts-expect-error duplicates are not allowed
  double1: { path: '/:id/:id', params: { id: v }, loader },
  // @ts-expect-error duplicates are not allowed
  double2: { path: '/:id/:id', loader },
  // @ts-expect-error duplicates are not allowed
  double3: { path: '/:id/:id/', params: { id: v }, loader },
  // @ts-expect-error duplicates are not allowed
  double4: { path: '/:id/:id/', loader },
  // @ts-expect-error duplicates are not allowed
  double5: { path: '/:id/:tab/:id', params: { id: v, tab: v }, loader },
  // @ts-expect-error duplicates are not allowed
  double6: { path: '/:id/:tab/:id', loader },
});

// @ts-expect-error internalError is required
createConfigs({ notFound: { path: '/404', loader } });

// @ts-expect-error notFound is required
createConfigs({ internalError: { path: '/500', loader } });

createConfigs({
  // @ts-expect-error "query" is not allowed
  notFound: { path: '/404', loader, query: {} },
  // @ts-expect-error "params" are not allowed
  internalError: { path: '/500', loader, params: { code: v } },
});

createConfigs({
  // @ts-expect-error "beforeLeave" is not allowed
  notFound: { path: '/404', loader, beforeLeave: () => undefined },
  // @ts-expect-error "beforeEnter" is not allowed
  internalError: { path: '/500', loader, beforeEnter: () => undefined },
});

Важно

В валидаторы приходят декодированные значения для удобства работы с не-английскими URL:

id: (value) => console.log(value) покажет не with%20space а with space

Таким образом, Reactive Route гарантирует, что все параметры в router.state.userDetails.params прошли валидаторы и их можно безопасно использовать.

В рантайме path всегда источник истины, и даже при отключенном TS стабильный матчинг гарантирован. Порядок объявления Config не важен, в алгоритме роутера только одно правило — при полном совпадении URL и path эта конфигурация обладает наивысшим приоритетом, даже если она объявлена в конце. В остальных сценариях выигрывает первый Config, у которого прошли все валидаторы.

config.query

Описываются в том же формате, что и params, в виде валидаторов:

ts
search: {
  path: '/search',
  query: {
    userPrompt: (value) => value.length > 5
  },
  loader: () => import('./pages/search')
}

Важно

В валидаторы попадают декодированные значения:

userPrompt: (value) => console.log(value) покажет не with%20space а with space

Все query параметры являются опциональными, и их отсутствие не приводит к редиректу на notFound.

ts
const pageState = router.state.search;

await router.redirect({ name: 'search', query: { userPrompt: 'short' }})

console.log(pageState.query.userPrompt) // undefined

await router.redirect({ name: 'search', query: { userPrompt: 'enough' }})

console.log(pageState.query.userPrompt) // 'enough'

При необходимости наличия определенных query можно их проверять в beforeEnter и редиректить на notFound если они не пришли в nextState.query.

config.beforeEnter

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

Аргументы
ts
reason: 
  | 'new_query' 
  | 'new_params' 
  | 'new_config'
Причина, по которой вызван beforeEnter. Если изменились и params и query, то new_params
ts
nextState: TypeStateUntyped
Следующий предполагаемый State
ts
currentState?: TypeStateUntyped
Текущий активный State (undefined при самом первом редиректе)
ts
redirect: (
  stateDynamic: 
    TypeStateDynamicUntyped &
    { replace?: boolean; }
) => void
Метод для редиректа внутри жизненного цикла. Так как createConfigs вызывается до создания роутера, здесь не получится использовать router.redirect
ts
dashboard: {
  path: '/dashboard',
  loader: () => import('./pages/dashboard'),
  async beforeEnter({ redirect }) {
    await api.loadUser();

    if (!store.isAuthenticated()) {
      return redirect({ name: 'login', query: { returnTo: 'dashboard' } });
    }

    await api.loadDashboard();
  }
}
Тесты типизации beforeEnter / beforeLeave
ts
import { createConfigs } from '../../packages/core';

const loader = async () => ({ default: null });

createConfigs({
  static: {
    path: '/',
    loader,
    async beforeEnter({ currentState, nextState, reason, redirect }) {
      currentState?.name;
      currentState?.params;
      currentState?.query;
      // @ts-expect-error unknown "currentState" property is not available
      currentState?.unknown;

      nextState.name;
      nextState.params;
      nextState.query;
      // @ts-expect-error unknown "nextState" property is not available
      nextState.unknown;

      reason satisfies 'unmodified' | 'new_query' | 'new_params' | 'new_config';
      // @ts-expect-error unknown reason is not assignable
      reason satisfies 'unknown';

      redirect({ name: 'static' });
      redirect({ name: 'static', replace: true });
      redirect({ name: 'unknown' });
      // @ts-expect-error "replace" must be boolean
      redirect({ name: 'static', replace: 'true' });
      // @ts-expect-error redirect params values must be strings
      redirect({ name: 'dynamic', params: { id: 1 } });
    },
    async beforeLeave({ currentState, nextState, reason, preventRedirect }) {
      currentState.name;
      currentState.params;
      currentState.query;
      // @ts-expect-error unknown "currentState" property is not available
      currentState.unknown;

      nextState.name;
      nextState.params;
      nextState.query;
      // @ts-expect-error unknown "nextState" property is not available
      nextState.unknown;

      reason satisfies 'unmodified' | 'new_query' | 'new_params' | 'new_config';
      // @ts-expect-error unknown reason is not assignable
      reason satisfies 'unknown';

      preventRedirect();
      // @ts-expect-error "preventRedirect" does not accept arguments
      preventRedirect(true);
    },
  },
  notFound: { path: '/404', loader },
  internalError: { path: '/500', loader },
});

Ограничения

Только в функциях жизненного цикла redirect, currentState и nextState имеют неполную типизацию (name - просто string) из-за ограничений TypeScript 5, поэтому при рефакторинге TS не выведет ошибок.

Совет

Всегда используйте return с redirect и preventRedirect для стабильной логики редиректов.

config.beforeLeave

Эту асинхронную функцию можно использовать для прерывания редиректа. Необработанные ошибки приведут к рендеру internalError без изменения URL в браузере.

Аргументы
ts
reason: 
  | 'new_query' 
  | 'new_params' 
  | 'new_config'
Причина, по которой вызван beforeLeave. Если изменились и params и query, то new_params
ts
nextState: TypeStateUntyped
Следующий предполагаемый State
ts
currentState: TypeStateUntyped
Текущий активный State
ts
preventRedirect: () => void
Метод для остановки редиректа
ts
dashboard: {
  path: '/dashboard',
  loader: () => import('./pages/dashboard'),
  async beforeLeave({ preventRedirect, nextState }) {
    if (nextState.name === 'login') return preventRedirect();

    // Do not check for unsaved changes on the server
    if (typeof window === 'undefined') return;

    const hasUnsavedChanges = await api.checkForm();

    if (hasUnsavedChanges && !window.confirm(
      `You have unsaved changes. Are you sure you want to leave?`
    )) return preventRedirect();
  }
}
Тесты типизации beforeEnter / beforeLeave
ts
import { createConfigs } from '../../packages/core';

const loader = async () => ({ default: null });

createConfigs({
  static: {
    path: '/',
    loader,
    async beforeEnter({ currentState, nextState, reason, redirect }) {
      currentState?.name;
      currentState?.params;
      currentState?.query;
      // @ts-expect-error unknown "currentState" property is not available
      currentState?.unknown;

      nextState.name;
      nextState.params;
      nextState.query;
      // @ts-expect-error unknown "nextState" property is not available
      nextState.unknown;

      reason satisfies 'unmodified' | 'new_query' | 'new_params' | 'new_config';
      // @ts-expect-error unknown reason is not assignable
      reason satisfies 'unknown';

      redirect({ name: 'static' });
      redirect({ name: 'static', replace: true });
      redirect({ name: 'unknown' });
      // @ts-expect-error "replace" must be boolean
      redirect({ name: 'static', replace: 'true' });
      // @ts-expect-error redirect params values must be strings
      redirect({ name: 'dynamic', params: { id: 1 } });
    },
    async beforeLeave({ currentState, nextState, reason, preventRedirect }) {
      currentState.name;
      currentState.params;
      currentState.query;
      // @ts-expect-error unknown "currentState" property is not available
      currentState.unknown;

      nextState.name;
      nextState.params;
      nextState.query;
      // @ts-expect-error unknown "nextState" property is not available
      nextState.unknown;

      reason satisfies 'unmodified' | 'new_query' | 'new_params' | 'new_config';
      // @ts-expect-error unknown reason is not assignable
      reason satisfies 'unknown';

      preventRedirect();
      // @ts-expect-error "preventRedirect" does not accept arguments
      preventRedirect(true);
    },
  },
  notFound: { path: '/404', loader },
  internalError: { path: '/500', loader },
});

Ограничения

Только в функциях жизненного цикла redirect, currentState и nextState имеют неполную типизацию (name - просто string) из-за ограничений TypeScript 5, поэтому при рефакторинге TS не выведет ошибок.

Совет

Всегда используйте return с redirect и preventRedirect для стабильной логики редиректов.

State

Реактивный объект, хранящийся в router.state.

Свойства
ts
name: string
Соответствует ключу Config
ts
params: Record<
  keyof TConfig['params'], 
  string
>
Проверенные и декодированные значения params. Все обязательно будут присутствовать
ts
query: Partial<Record<
  keyof TConfig['query'], 
  string
>>
Проверенные и декодированные значения query. Все опциональны
Тесты типизации State
ts
import { createConfigs, createRouter } from '../../packages/core';

const loader = async () => ({ default: null });

const v: (value: string) => boolean = () => true;

const adapters = {} as any;

const configs = createConfigs({
  static: { path: '/', loader },
  staticQuery: { path: '/', query: { q: v }, loader },
  dynamic: { path: '/:id', params: { id: v }, loader },
  dynamicQuery: {
    path: '/:id',
    params: { id: v },
    query: { q: v },
    loader,
  },
  notFound: { path: '/404', loader },
  internalError: { path: '/500', loader },
});

const router = createRouter({ configs, adapters });

// @ts-expect-error not existing name
router.state.unknown;

router.state.static!.name;
router.state.static!.params;
router.state.static!.query;
// @ts-expect-error "params" values are not available
router.state.static!.params.unknown;
// @ts-expect-error "query" values are not available
router.state.static!.query.unknown;

router.state.notFound!.name;
router.state.notFound!.params;
// @ts-expect-error "params" values are not available
router.state.notFound!.params.unknown;
router.state.notFound!.query;
// @ts-expect-error "query" values are not available
router.state.notFound!.query.unknown;

router.state.internalError!.name;
router.state.internalError!.params;
// @ts-expect-error "params" values are not available
router.state.internalError!.params.unknown;
router.state.internalError!.query;
// @ts-expect-error "query" values are not available
router.state.internalError!.query.unknown;

router.state.staticQuery!.name;
router.state.staticQuery!.params;
// @ts-expect-error "params" values are not available
router.state.staticQuery!.params.unknown;
router.state.staticQuery!.query.q;
// @ts-expect-error unknown "query" are not available
router.state.staticQuery!.query.unknown;

router.state.dynamic!.name;
router.state.dynamic!.params.id;
// @ts-expect-error unknown "params" are not available
router.state.dynamic!.params.unknown;
router.state.dynamic!.query;
// @ts-expect-error "query" values are not available
router.state.dynamic!.query.unknown;

router.state.dynamicQuery!.name;
router.state.dynamicQuery!.params.id;
router.state.dynamicQuery!.query.q;
// @ts-expect-error unknown "params" are not available
router.state.dynamicQuery!.params.unknown;
// @ts-expect-error unknown "query" are not available
router.state.dynamicQuery!.query.unknown;
Type Narrowing для State
ts
import { createConfigs, createRouter } from '../../packages/core';

const loader = async () => ({ default: null });

const v: (value: string) => boolean = () => true;

const adapters = {} as any;

const configs = createConfigs({
  static: { path: '/', loader },
  staticQuery: { path: '/', query: { q: v }, loader },
  dynamic: { path: '/:id', params: { id: v }, loader },
  dynamicQuery: {
    path: '/:id',
    params: { id: v },
    query: { q: v },
    loader,
  },
  notFound: { path: '/404', loader },
  internalError: { path: '/500', loader },
});

const router = createRouter({ configs, adapters });

const state = router.urlToState('');

// @ts-expect-error not existing name
state.name === 'unknown';

if (state.name === 'notFound') {
  state.name;
  state.params;
  // @ts-expect-error "params" values are not available
  state.params.unknown;
  state.query;
  // @ts-expect-error "query" values are not available
  state.query.unknown;
}

if (state.name === 'internalError') {
  state.name;
  state.params;
  // @ts-expect-error "params" values are not available
  state.params.unknown;
  state.query;
  // @ts-expect-error "query" values are not available
  state.query.unknown;
}

if (state.name === 'static') {
  state.name;
  state.params;
  state.query;
  // @ts-expect-error "params" values are not available
  state.params.unknown;
  // @ts-expect-error "query" values are not available
  state.query.unknown;
}

if (state.name === 'staticQuery') {
  state.name;
  state.params;
  // @ts-expect-error "params" values are not available
  state.params.unknown;
  state.query.q;
  // @ts-expect-error unknown "query" are not available
  state.query.unknown;
}

if (state.name === 'dynamic') {
  state.name;
  state.params.id;
  // @ts-expect-error unknown "params" are not available
  state.params.unknown;
  state.query;
  // @ts-expect-error "query" values are not available
  state.query.unknown;
}

if (state.name === 'dynamicQuery') {
  state.name;
  state.params.id;
  state.query.q;
  // @ts-expect-error unknown "params" are not available
  state.params.unknown;
  // @ts-expect-error unknown "query" are not available
  state.query.unknown;
}

Router

createRouter

Эта функция создает router.

Аргументы
ts
configs: ReturnType<typeof createConfigs>
Объект с Configs
ts
adapters: TypeAdapters
Адаптеры для системы реактивности
ts
beforeComponentChange?: (params: {
  prevState?: TypeState;
  prevConfig?: TypeConfig;

  currentState: TypeState;
  currentConfig: TypeConfig;
}) => void
Глобальная функция жизненного цикла, которая выполняется только при изменении компонента (не страницы!)

beforeComponentChange

Эта функция вызывается только при изменении отрендеренного компонента и предназначена для использования в модульных архитектурах.

ts
// Some page exports "export class PageStore { data: {}, destroy() {} }"

const globalStore = { pages: {} };

createRouter({
  configs,
  adapters,
  beforeComponentChange({ prevConfig, currentConfig }) {
    // PageStore is accessible from the otherExports object
    const { PageStore } = currentConfig.otherExports;

    if (PageStore) globalStore.pages[currentConfig.name] = new PageStore();

    // destroy the obsolete PageStore from the previuos page
    globalStore.pages[prevConfig.name]?.destroy();
  }
})
Тесты типизации beforeComponentChange
ts
import { createConfigs, createRouter } from '../../packages/core';

const loader = async () => ({ default: null });

const v: (value: string) => boolean = () => true;

const adapters = {} as any;

const configs = createConfigs({
  static: { path: '/', loader },
  staticQuery: { path: '/', query: { q: v }, loader },
  dynamic: { path: '/:id', params: { id: v }, loader },
  dynamicQuery: {
    path: '/:id',
    params: { id: v },
    query: { q: v },
    loader,
  },
  notFound: { path: '/404', loader },
  internalError: { path: '/500', loader },
});

createRouter({
  configs,
  adapters,
  beforeComponentChange({ prevConfig, prevState, currentConfig, currentState }) {
    /** prevConfig */

    prevConfig?.name;
    prevConfig?.loader;
    // @ts-expect-error unknown prevConfig property is not available
    prevConfig?.unknown;

    if (prevConfig?.name === 'dynamic') {
      prevConfig.params.id;
      // @ts-expect-error unknown "params" are not available
      prevConfig.params.unknown;
      // @ts-expect-error "query" is not available
      prevConfig.query.q;
    }

    if (prevConfig?.name === 'static') {
      // @ts-expect-error "params" is not available
      prevConfig.params.id;
      // @ts-expect-error "query" is not available
      prevConfig.query.q;
    }

    if (prevConfig?.name === 'staticQuery') {
      prevConfig.query.q;
      // @ts-expect-error unknown "query" are not available
      prevConfig.query.unknown;
      // @ts-expect-error "params" is not available
      prevConfig.params.id;
    }

    if (prevConfig?.name === 'dynamicQuery') {
      prevConfig.params.id;
      prevConfig.query.q;
      // @ts-expect-error unknown "params" are not available
      prevConfig.params.unknown;
      // @ts-expect-error unknown "query" are not available
      prevConfig.query.unknown;
    }

    if (prevConfig?.name === 'notFound') {
      // @ts-expect-error "params" is not available
      prevConfig.params.code;
      // @ts-expect-error "query" is not available
      prevConfig.query.q;
    }

    if (prevConfig?.name === 'internalError') {
      // @ts-expect-error "params" is not available
      prevConfig.params.code;
      // @ts-expect-error "query" is not available
      prevConfig.query.q;
    }

    /** currentConfig */

    currentConfig.name;
    currentConfig.loader;
    // @ts-expect-error unknown currentConfig property is not available
    currentConfig.unknown;

    if (currentConfig.name === 'notFound') {
      // @ts-expect-error "params" is not available
      currentConfig.params.code;
      // @ts-expect-error "query" is not available
      currentConfig.query.q;
    }

    if (currentConfig.name === 'internalError') {
      // @ts-expect-error "params" is not available
      currentConfig.params.code;
      // @ts-expect-error "query" is not available
      currentConfig.query.q;
    }

    if (currentConfig.name === 'static') {
      // @ts-expect-error "params" is not available
      currentConfig.params.id;
      // @ts-expect-error "query" is not available
      currentConfig.query.q;
    }

    if (currentConfig.name === 'staticQuery') {
      currentConfig.query.q;
      // @ts-expect-error unknown "query" are not available
      currentConfig.query.unknown;
      // @ts-expect-error "params" is not available
      currentConfig.params.id;
    }

    if (currentConfig.name === 'dynamicQuery') {
      currentConfig.params.id;
      currentConfig.query.q;
      // @ts-expect-error unknown "params" are not available
      currentConfig.params.unknown;
      // @ts-expect-error unknown "query" are not available
      currentConfig.query.unknown;
    }

    if (currentConfig.name === 'dynamic') {
      currentConfig.params.id;
      // @ts-expect-error unknown "params" are not available
      currentConfig.params.unknown;
      // @ts-expect-error "query" is not available
      currentConfig.query.q;
    }

    /** prevState */

    prevState?.name;
    prevState?.params;
    prevState?.query;
    // @ts-expect-error unknown prevState property is not available
    prevState?.unknown;

    // @ts-expect-error not existing name
    prevState?.name === 'unknown';

    if (prevState?.name === 'staticQuery') {
      prevState.query.q;
      // @ts-expect-error unknown "query" are not available
      prevState.query.unknown;
      // @ts-expect-error "params" values are not available
      prevState.params.unknown;
    }

    if (prevState?.name === 'notFound') {
      prevState.params;
      // @ts-expect-error "params" values are not available
      prevState.params.unknown;
      prevState.query;
      // @ts-expect-error "query" values are not available
      prevState.query.unknown;
    }

    if (prevState?.name === 'internalError') {
      prevState.params;
      // @ts-expect-error "params" values are not available
      prevState.params.unknown;
      prevState.query;
      // @ts-expect-error "query" values are not available
      prevState.query.unknown;
    }

    if (prevState?.name === 'static') {
      prevState.params;
      prevState.query;
      // @ts-expect-error "params" values are not available
      prevState.params.unknown;
      // @ts-expect-error "query" values are not available
      prevState.query.unknown;
    }

    if (prevState?.name === 'dynamic') {
      prevState.params.id;
      // @ts-expect-error unknown "params" are not available
      prevState.params.unknown;
      prevState.query;
      // @ts-expect-error "query" values are not available
      prevState.query.unknown;
    }

    if (prevState?.name === 'dynamicQuery') {
      prevState.params.id;
      prevState.query.q;
      // @ts-expect-error unknown "params" are not available
      prevState.params.unknown;
      // @ts-expect-error unknown "query" are not available
      prevState.query.unknown;
    }

    /** currentState */

    currentState.name;
    currentState.params;
    currentState.query;
    // @ts-expect-error unknown currentState property is not available
    currentState.unknown;

    // @ts-expect-error not existing name
    currentState.name === 'unknown';

    if (currentState.name === 'notFound') {
      currentState.params;
      // @ts-expect-error "params" values are not available
      currentState.params.unknown;
      currentState.query;
      // @ts-expect-error "query" values are not available
      currentState.query.unknown;
    }

    if (currentState.name === 'internalError') {
      currentState.params;
      // @ts-expect-error "params" values are not available
      currentState.params.unknown;
      currentState.query;
      // @ts-expect-error "query" values are not available
      currentState.query.unknown;
    }

    if (currentState.name === 'static') {
      currentState.params;
      currentState.query;
      // @ts-expect-error "params" values are not available
      currentState.params.unknown;
      // @ts-expect-error "query" values are not available
      currentState.query.unknown;
    }

    if (currentState.name === 'staticQuery') {
      currentState.query.q;
      // @ts-expect-error unknown "query" are not available
      currentState.query.unknown;
      // @ts-expect-error "params" values are not available
      currentState.params.unknown;
    }

    if (currentState.name === 'dynamic') {
      currentState.params.id;
      // @ts-expect-error unknown "params" are not available
      currentState.params.unknown;
      // @ts-expect-error "query" values are not available
      currentState.query.unknown;
    }

    if (currentState.name === 'dynamicQuery') {
      currentState.params.id;
      currentState.query.q;
      // @ts-expect-error unknown "params" are not available
      currentState.params.unknown;
      // @ts-expect-error unknown "query" are not available
      currentState.query.unknown;
    }
  },
});

Таким образом страница user может получать доступ к своему PageStore через globalStore.pages.user. Это позволяет более эффективно использовать code-splitting и сериализовывать только globalStore при SSR - в нем уже будут данные для необходимой страницы.

Также эту функцию можно использовать для прерывания асинхронных операций и подписок.

router.redirect

Выполняет полный цикл редиректа, подробнее описано в принципах работы. Если передать дополнительное свойство replace: true, то последний элемент истории браузера будет заменен. Возвращает строку с новым URL.

Вторым аргументом принимает skipLifecycle?: boolean если нужно пропустить вызовы beforeEnter и beforeLeave.

ts
const newUrl = await router.redirect({
  name: 'user',
  params: { id: '9999' },
  query: { phone: '123456' }
})
// '/user/9999?phone=123456'
Тесты типизации StateDynamic
ts
import { createConfigs, createRouter } from '../../packages/core';

const loader = async () => ({ default: null });

const v: (value: string) => boolean = () => true;

const adapters = {} as any;

const configs = createConfigs({
  static: { path: '/', loader },
  staticQuery: { path: '/', query: { q: v }, loader },
  dynamic: { path: '/:id', params: { id: v }, loader },
  dynamicQuery: {
    path: '/:id',
    params: { id: v },
    query: { q: v },
    loader,
  },
  notFound: { path: '/404', loader },
  internalError: { path: '/500', loader },
});

const router = createRouter({ configs, adapters });

await router.redirect({ name: 'static' });
await router.redirect({ name: 'staticQuery' });
await router.redirect({ name: 'staticQuery', query: { q: '' } });
await router.redirect({ name: 'dynamic', params: { id: '' } });
await router.redirect({ name: 'dynamicQuery', params: { id: '' } });
await router.redirect({
  name: 'dynamicQuery',
  params: { id: '' },
  query: { q: '' },
});

/** Not string values raise errors */

await router.redirect({
  // @ts-expect-error name must be a string
  name: 1,
});
await router.redirect({
  name: 'staticQuery',
  // @ts-expect-error "query" values must be strings
  query: { q: 1 },
});
await router.redirect({
  name: 'dynamic',
  // @ts-expect-error "params" values must be strings
  params: { id: 1 },
});
await router.redirect({
  name: 'dynamicQuery',
  params: { id: '' },
  // @ts-expect-error "query" values must be strings
  query: { q: 1 },
});
// @ts-expect-error "params" are required
await router.redirect({ name: 'dynamic' });
await router.redirect({
  name: 'dynamic',
  // @ts-expect-error "params" are incomplete
  params: {},
});

/** Irrelevant StateDynamic raise errors */

// @ts-expect-error name must be present
await router.redirect({});
// @ts-expect-error "name" must be a string key of configs
await router.redirect({ name: 'unknown' });
await router.redirect({
  name: 'static',
  // @ts-expect-error "params" are not allowed
  params: { id: '' },
});
await router.redirect({
  name: 'staticQuery',
  // @ts-expect-error "params" are not allowed
  params: { id: '' },
});
await router.redirect({
  name: 'dynamic',
  // @ts-expect-error extra "params" are not allowed
  params: { id: '', extra: '' },
});
await router.redirect({
  name: 'static',
  // @ts-expect-error "query" is not allowed
  query: { q: '' },
});
await router.redirect({
  name: 'dynamic',
  params: { id: '' },
  // @ts-expect-error "query" is not allowed
  query: { q: '' },
});
await router.redirect({
  name: 'staticQuery',
  // @ts-expect-error unknown "query" key
  query: { unknown: '' },
});
await router.redirect({
  name: 'dynamicQuery',
  params: { id: '' },
  // @ts-expect-error unknown "query" key
  query: { unknown: '' },
});

const state = router.urlToState('');

void router.redirect(state);

router.urlToState

Принимает URL и возвращает State с фолбэком на notFound.

ts
router.urlToState(`/user/9999?phone=123456&gtm=value`);

// {
//   name: 'user',
//   params: { id: '9999' },
//   query: { phone: '123456' }
// }

router.urlToState(`/not-existing/admin?hacker=sql-inject`);

// { 
//  name: 'notFound', 
//  params: {}, 
//  query: {}
// }

Дополнительно

Сохраняются только описанные query, прошедшие валидацию, в данном случае gtm не попал в State.

router.init

Сокращенная форма router.redirect(router.urlToState(url)). Вторым аргументом принимает skipLifecycle?: boolean если нужно пропустить вызовы beforeEnter и beforeLeave.

ts
// browser usage example:
await router.init(location.href)

// browser usage with SSR example:
await router.init(location.href, { skipLifecycle: true })

// Express.js server usage example:
const newUrl = await router.init(req.originalUrl)

// Optional step if you want to clear irrelevant query
if (req.originalUrl !== newUrl) res.redirect(newUrl)

router.state

Реактивный объект, ключами которого являются name, а значениями — State, например:

ts
console.log(router.state.user);

// reactive object
// {
//   name: 'user',
//   params: { id: '9999' },
//   query: { phone: '123456' }
// }

Предназначен для отображения значений в UI и для описания логики в autoruns/effects. При редиректе с новыми params или query эти значения соответственно изменятся в router.state.user.

Роутер не уничтожает старый State при переходе на другой Config. В данном примере если перейти на router.redirect({ name: 'home' }), все равно будет присутствовать router.state.user. Это помогает решить проблему с неочищенными подписками на старое состояние в рантайме.

Если бы Reactive Route использовал хранение только одного активного router.getActiveState() (это несуществующий метод!), как многие нереактивные роутеры, то подписка запустилась бы до unmount компонента с некорректным State, в котором может не быть этих параметров.

tsx
// with mobx adapter

import { autorun } from 'mobx'

function PageUser() {
  const { router } = useContext(RouterContext);

  const pageState = router.state.user!;
  
  useEffect(() => autorun(() => {
    console.log(pageState.params.id, pageState.query.phone);
  }), []);
}
tsx
// with kr-observable adapter

import { autorun } from 'kr-observable'

function PageUser() {
  const { router } = useContext(RouterContext);

  const pageState = router.state.user!;
  
  useEffect(() => autorun(() => {
    console.log(pageState.params.id, pageState.query.phone);
  }), []);
}
tsx
// with solid adapter

import { createRenderEffect } from 'solid-js'

function PageUser() {
  const { router } = useContext(RouterContext);

  const pageState = router.state.user!;

  createRenderEffect(() => {
    console.log(pageState.params.id, pageState.query.phone);
  })
}
vue
<script lang="ts" setup>
  // with vue adapter
  
  import { watchEffect } from 'vue';

  const { router } = useRouterStore();

  const pageState = router.state.user!;

  watchEffect(() => {
    console.log(pageState.params.id, pageState.query.phone);
  });
</script>

router.isRedirecting

Реактивный boolean для отображения индикаторов загрузки при редиректах. Ниже показаны примеры глобального и локального отображения:

tsx
function GlobalLoader() {
  const { router } = useContext(RouterContext);

  return router.isRedirecting ? <Loader/> : null;
}

function SomeComponent() {
  const { router } = useContext(RouterContext);

  return <Button isLoading={router.isRedirecting}>Send</Button>;
}
tsx
function GlobalLoader() {
  const { router } = useContext(RouterContext);

  return router.isRedirecting ? <Loader/> : null;
}

function SomeComponent() {
  const { router } = useContext(RouterContext);

  return <Button isLoading={router.isRedirecting}>Send</Button>;
}
tsx
function GlobalLoader() {
  const { router } = useContext(RouterContext);

  return <Show when={router.isRedirecting}><Loader/></Show>;
}

function SomeComponent() {
  const { router } = useContext(RouterContext);

  return <Button isLoading={router.isRedirecting}>Send</Button>;
}
vue
<script lang="ts" setup>
  import { useRouter } from '../../router';

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

<template>
  <Loader v-if="router.isRedirecting"/>

  <Button :is-loading="router.isRedirecting">Send</Button>
</template>

router.activeName

Реактивный name активного State (undefined до самого первого редиректа).

router.preloadComponent

Reactive Route загружает чанки страниц (выполняет loader) только во время редиректов. Эта функция может использоваться для предварительной загрузки и принимает name

ts
await router.init(location.href);

setTimeout(async () => {
  try {
    await router.preloadComponent('login')
    await router.preloadComponent('dashboard')
  } catch (e) {
    console.error('Seems like the user lost connection')
  }
}, 5000)

router.getGlobalArguments

Позволяет получить конфигурацию, переданную в createRouter.

No AI participated in the development. MIT License