<p align="center"> <img style="background-color: white; border-radius: 38px;" width="110px" height="110px" src="https://storage.yandexcloud.net/astral-frontend/mobx-router-logo.png"> </p>

Абстрактный реактивный router service, позволяющий использовать один интерфейс взаимодействия для разных роутеров:
- react-router v6 и v7
- nextjs router
- Getting Started
- React Router v6/v7
- Next.js (Pages Router)
- Использование
- Определение routes
- Навигация (navigate)
- Генерация ссылок
- RouteParams
- Навигация
- Типобезопасная навигация
- Метод navigate
- Асинхронный переход navigateAsync
- Возврат по истории назад (back)
- Перезагрузка страницы (reload)
- State при навигации
- Работа с SearchParams
- Типобезопасные изолированные searchParams
- Values
- Валидация
- Валидация дат
- Установка значений (set, setAll)
- Удаление параметров (delete)
- Сброс значений (reset)
- Создание нескольких searchParamsStore
- initialValues
- defaultValues
- Особенности работы с nextjs
- Не типизированные searchParams, приведенные к js структурам
- Парсинг и сериализация searchParams
- Кастомный парсинг и сериализация searchParams
- Сохранение Search параметров при навигации
- Блокировка маршрутов (blocker)
- Состояние блокировки
- Управления блокировкой
- Работа с несколькими блокерами
- Управление жизненным циклом (destroy)
- MatchPath - Сопоставление маршрутов
- API
- Базовый пример
- Полное сопоставление
- Практические примеры
- Location
- API
- pathname
- SubscribeRoute
- Debug
- Тестирование
#### Установка
``bash`
npm install @astral/mobx-router @astral/mobx-router-react mobx react-router
#### Подготовка
- Для работы с react-router v6/v7 необходимо использовать DataRouter. Ниже описан пример настройки
- Все импорты должны быть из react-router, а не из react-router-dom
`typescript
// ✅
import { createBrowserRouter, useLocation } from 'react-router';
// ❌
import { createBrowserRouter, useLocation } from 'react-router-dom';
`
#### Настройка
Важно: Для работы с react-router v6/v7 необходимо использовать DataRouter.
`shared/services/Router``typescript
import { MobxRouter } from '@astral/mobx-router';
import { ReactRouterProvider } from '@astral/mobx-router-react';
export const reactRouterProvider = new ReactRouterProvider();
const routesConfig = {
home: MobxRouter.defineRoute({
pattern: '/home',
}),
user: MobxRouter.defineRoute({
pattern: '/user/:id',
generatePath: (params: { id: string }) => /user/${params.id},/profile/${userId}
}),
profile: MobxRouter.defineRoute({
pattern: '/profile/:userId?',
generatePath: ({ userId }: { userId?: string }) => userId ? : '/profile' ,
}),
};
export const router = new MobxRouter(reactRouterProvider, routesConfig);
// Необходим для инжектирования в MobX store, чтобы в типах были routes приложения
export type Router = typeof router;
`
`application/routes.tsx``typescript
import { lazy, Suspense } from 'react';
import { ProviderAdapter } from '@astral/mobx-router-react';
import { createBrowserRouter, Outlet } from 'react-router';
import { MainLayout } from '#modules/layout';
import { ContentState, reactRouterProvider, router } from '#shared';
const IndexPage = lazy(() => import('./pages/index'));
const HomePage = lazy(() => import('./pages/home'));
const UserPage = lazy(() => import('./pages/user'));
const ProfilePage = lazy(() => import('./pages/profile'));
export const browserRouter = createBrowserRouter([
{
element: (
<>
>
>
),
children: [
{
path: '/',
element:
},
{
path: router.routes.home.pattern,
element:
},
{
path: router.routes.user.pattern,
element:
},
{
path: router.routes.profile.pattern,
element:
},
],
},
]);
`
`application/app``typescript
import { ProviderAdapter } from '@astral/mobx-router-react';
import { RouterProvider as ReactRouterProvider } from 'react-router/dom';
import { browserRouter } from './routes';
const App = () => {
return (
);
};
`
Поддерживает только pages routing
#### Установка
`bash`
npm install @astral/mobx-router @astral/mobx-router-nextjs-pages mobx
#### Настройка
`shared/services/Router``typescript
import { MobxRouter } from '@astral/mobx-router';
import { NextjsPagesRouterProvider } from '@astral/mobx-router-nextjs-pages';
const provider = new NextjsPagesRouterProvider();
const routesConfig = {
home: MobxRouter.defineRoute({
pattern: '/home',
}),
user: MobxRouter.defineRoute({
pattern: '/user/[id]',
generatePath: (params: { id: string }) => /user/${params.id} ,/profile/${userId}
}),
profile: MobxRouter.defineRoute({
pattern: '/profile/[[userId]]',
generatePath: ({ userId }: { userId?: string }) => userId ? : '/profile' ,
}),
};
export const router = new MobxRouter(provider, routesConfig);
// Необходим для инжектирования в MobX store, чтобы в типах были routes приложения
export type Router = typeof router;
`
`pages/_app.tsx``typescript
import { ProviderAdapter } from '@astral/mobx-router-nextjs-pages';
import type { AppProps } from 'next/app';
import { provider } from '#shared/services/Router';
function MyApp({ Component, pageProps }: AppProps) {
return (
<>
>
);
}
export default MyApp;
`
После настройки вы можете использовать роутер для навигации:
`typescript
// Простой переход
router.navigate('/dashboard');
// Переход с заменой истории
router.navigate('/login', { replace: true });
`
Или подписываться на изменения роутера в mobx:
`typescript
import { makeAutoObservable } from 'mobx';
import type { Router } from '#shared';
class Store {
constructor(private readonly _router: Router) {
makeAutoObservable(this);
}
get isAdminPage() {
return this._router.location.pathname.startsWith('/admin');
}
}
`
После создания роутера необходимо определить маршруты приложения:
- Для статических маршрутов достаточно указать только pattern. Он будет использоваться как pathname для переходовgeneratePath
- Для динамических маршрутов с использованием routeParams необходимо определять . Если generatePath не будет принимать параметров, возникнет исключение
`typescript
import { MobxRouter } from '@astral/mobx-router';
// Определяем конфигурацию routes
const routesConfig = {
// Для статичных путей без параметров достаточно указать pattern
home: MobxRouter.defineRoute({
pattern: '/home', // pathname будет '/home'
}),
// Для путей с параметрами рекомендуется указывать generatePath
user: MobxRouter.defineRoute({
pattern: '/user/:id',
generatePath: (params: { id: string }) => /user/${params.id} ,/profile/${userId}
}),
profile: MobxRouter.defineRoute({
pattern: '/profile/:userId?',
generatePath: ({ userId }: { userId?: string }) => userId ? : '/profile' ,
}),
};
// Создаем роутер с конфигурацией
export const router = new MobxRouter(provider, routesConfig);
`
Интерфейс navigate аналогичен интерфейсу корневого navigate.
После определения routes вы можете использовать их для типобезопасной навигации:
`typescript
// Простая навигация без параметров
router.routes.home.navigate();
// Навигация с обязательными параметрами
router.routes.user.navigate({ params: { id: '123' } });
// Навигация с опциональными параметрами
router.routes.profile.navigate(); // без параметров
router.routes.profile.navigate({ params: { userId } }); // с параметрами
`
Каждый маршрут также предоставляет доступ к своим параметрам через свойство routeParams:
`typescript
type Params = { id: string };
const routesConfig = {
user: Router.defineRoute({
pattern: '/user/:id',
generatePath: (params: Params) => /user/${params.id},
}),
};
// Получение параметров конкретного маршрута
router.routes.user.routeParams; // { id: '123' }
`
В примере выше router.routes.user.routeParams будет соответствовать типу:`typescript`
{ id: string } | undefined
undefined, если маршрут не активен.
Каждый route предоставляет свойство isActive, которое позволяет определить, является ли данный route активным в текущий момент. Это свойство является MobX observable и автоматически обновляется при изменении URL.
#### Синтаксис
`typescript`
router.routes.routeName.isActive: boolean
#### Пример
`typescript
import { MobxRouter } from '@astral/mobx-router';
const routesConfig = {
user: MobxRouter.defineRoute({
pattern: '/user/:id',
generatePath: ({ id }: { id: string }) => /user/${params.id},
}),
};
export const router = new MobxRouter(provider, routesConfig);
...
`
`typescript
import { makeAutoObservable } from 'mobx';
class SidebarStore {
constructor(private readonly _router: Router) {
makeAutoObservable(this);
}
get isUserRouteActive() {
return this._router.routes.user.isActive;
}
}
`
Для создания ссылок на route используйте метод generateHref:
`typescript
const profileLink = router.routes.profile.generateHref({
userId: '456',
}); // /profile/456
// Использование в компонентах
Мой профиль
`
#### Генерация ссылки с searchParams
`typescript
const userLink = router.routes.user.generateHref(
{ id: '123' },
{ searchParams: { page: 1 } },
); // /user/123?page=1
// Использование в компонентах
Мой профиль;
`
Роутер предоставляет доступ к параметрам текущего активного маршрута через свойство routeParams:
`typescript/user/${params.id}
const routesConfig = {
user: Router.defineRoute({
pattern: '/user/:id',
generatePath: (params: { id: string }) => ,
}),
};
const router = new Router(provider, routesConfig);
// Получение параметров текущего активного маршрута
console.log(router.routeParams); // { id: '123' }
`
Основной метод для навигации между страницами приложения.
#### Синтаксис
`typescript`
router.navigate(to: string | Location, options?: NavigateOptions)
#### Параметры
- to (string | Location) - путь для перехода (строка или объект)options
- (NavigateOptions, опционально) - дополнительные параметры навигации
#### Тип NavigateOptions
`typescriptlocation.state
type NavigateOptions = {
/**
* Позволяет сделать переход без сохранения в history
*/
replace?: boolean;
/**
* @sse https://developer.mozilla.org/en-US/docs/Web/API/History/scrollRestoration
*/
scrollRestoration?: 'auto' | 'manual';
/**
* SearchParams в виде plain object для перехода
*/
searchParams?: SearchParams;
/**
* Дополнительные данные, которые будут переданы в `
* при навигации (поддерживается только в react-router).
*
* ⚠️ В Next.js Router этот параметр не поддерживается
*/
state?: unknown;
};
#### Примеры использования
`typescript
// Простой переход на страницу (строка)
router.navigate('/main');
// Переход с заменой текущей записи в истории браузера
router.navigate('/main', { replace: true });
// Переход с отключением автоматической прокрутки
router.navigate('/main', { scrollRestoration: 'manual' });
// Комбинирование параметров
router.navigate('/main', {
replace: true,
scrollRestoration: 'manual'
});
`
#### Переход с установкой searchParams
`typescript`
router.navigate({
pathname: '/user/123',
// ?page=1&filters=[1,2]
searchParams: { page: 1, filters: [1, 2] }
});
#### Передача search строки
Search можно передавать как ?, так и без:`typescript
// Переход с search параметрами (с ?)
router.navigate({
pathname: '/search',
search: '?query=test&page=1'
});
// Переход с search параметрами (без ?)
router.navigate({
pathname: '/search',
search: 'query=test&page=1'
});
`
#### Особенности реализации
- navigate не синхронный:
`typescript`
router.location.pathname; // /main
router.navigate('/user');
router.location.pathname; // /user
#### Асинхронный переход navigateAsync
Асинхронный метод для перехода между страницами приложения, который позволяет дождаться завершения навигации.
Завершение навигации - успешный рендеринг нового маршрута.
`typescript
// Переход на страницу и ожидание завершения
await router.navigateAsync('/main');
// Переход с заменой текущей записи в истории
await router.navigateAsync('/main', { replace: true });
// Переход с отключением автоматической прокрутки
await router.navigateAsync('/main', { scrollRestoration: 'manual' });
// Переход с передачей searchParams
await router.navigateAsync({
pathname: '/user/123',
searchParams: { page: 1, filters: [1, 2] }
});
`
#### Возврат по истории назад (back)
Метод позволяющий вернутся на предыдущий URL в истории
`typescript
router.navigate('/user'); // /main -> /user
router.back(); // возврат на /main
`
#### Перезагрузка страницы (reload)
Метод перезагружает текущую страницу
`typescript`
router.reload();
#### state при навигации
О работе state можно ознакомиться здесь
##### Особенности реализации
- NextRouter: параметр state не поддерживается
##### Тип NavigationState
`typescript`
type NavigationState = {
/**
* Информация о предыдущем location, с которого был совершен переход
*/
referer: {
/**
* Путь предыдущего location
*/
pathname: string;
/**
* search предыдущего location
*/
search?: string;
};
} & Record
##### Пример использования
`typescript
// Навигация с передачей state и сохранением referer
router.routes.user.navigate({
params: { id: '123' },
state: { filter: 'value' },
});
router.location
// {
// pathname: '/user/123',
// search: '',
// state: { referer: { pathname: currentLocation.pathname, search: currentLocation.search }, filter: 'value' },
// }
`
Роутер предоставляет несколько способов работы с search параметрами URL.
#### Базовая концепция
- Типобезопасность: router.createSearchParamsStore гарантирует, что values содержит именно те типы данных, которые были переданы в generic и заданы через валидацию.router.createSearchParamsStore
- Изолированность: работает только с теми параметрами, которые были переданы в generic и заданы через валидацию, никак не влияя при этом на другие searchParams.
#### Пример использования
`typescript
import { makeAutoObservable } from 'mobx';
import * as v from '@astral/validations';
import type {Router} from '#shared';
type SearchParams = {
id: string;
page?: number;
filters?: string[];
};
class UIStore {
private readonly searchStore;
public constructor(private readonly _router: Router) {
makeAutoObservable(this, {}, {autoBind: true});
this.searchStore = this._router.createSearchParamsStore
validationSchema: {
id: v.string(),
page: v.optional(v.number()),
filters: v.optional(v.array(v.arrayItem(v.string()))),
},
});
}
public get userId() {
// Values === undefined, если валидация не прошла
return this.searchStore.values?.id;
}
public get currentPage() {
return this.searchStore.values?.page || 1;
}
public get activeFilters() {
return this.searchStore.values?.filters || [];
}
public setUserId(id: string) {
this.searchStore.set('id', id);
}
public setFilters = (filters: string[]) => {
this.searchStore.set('filters', filters);
}
public deleteFilters = () => {
this.searchStore.delete('filters');
}
}
`
#### Values
store.values является observable.
#### Валидация
Валидация происходит:
- При инициализации SearchParamsStorevalues
- При обращении к или validationErrorssearchParams
- При изменении в рамках текущего store или в глобальном router.searchParams
store.values содержат:undefined
- если валидация завершилась ошибкой
- значения после парсинга если валидация завершилась без ошибок
validationSchema принимает схему объекта для @astral/validations.
##### Валидация при инициализации SearchParamsStore
Если валидация при инициализации SearchParamsStore завершилась ошибкой, то values будут содержать undefined, при этом router.searchParams не изменится если не заданы defaultValues.
##### Валидация при обращении к values
Валидация при обращении к values никак не меняет router.searchParams.
##### Валидация дат
Для валидации дат необходимо использовать метод transform
`typescript
import * as v from '@astral/validations';
type SearchParams = {
date: string;
};
const searchStore = router.createSearchParamsStore
validationSchema: {
date: v.string(v.transform((value) => new Date(value))),
},
})
// values === undefined, если валидация не прошла
searchStore.values?.date;
`
##### Обработка ошибок валидации
Если валидация завершилась ошибкой, то values будут содержать undefined, а validationErrors объект с информацией об ошибке:
`ts
import * as v from '@astral/validations';
type SearchParams = {
id: string;
};
// В url search отсутствует id
const searchStore = router.createSearchParamsStore
validationSchema: {
id: v.string(),
},
});
searchStore.values; // undefined
searchStore.validationErrors; // { id: { message: 'Не является строкой', code: 'astral-validations-string' } }
if (searchStore.validationErrors?.id.code === v.STRING_TYPE_ERROR_INFO.code) {
console.log('Передан не корректный id. Попробуйте...');
}
`
---
#### Установка значений (set, setAll)
SearchParamsStore предоставляет несколько методов для изменения параметров.
По умолчанию все изменения не сохраняются в истории браузера (используется replace: true).
##### set
Устанавливает значение для конкретного параметра, сохраняя остальные параметры без изменений.
`typescript
// Установка обязательного параметра
searchStore.set('id', '123');
// Установка массива
searchStore.set('filters', ['active', 'verified']);
// Сохранение в истории браузера
searchStore.set('page', 3, { replace: false });
`
##### setAll
Устанавливает все параметры сразу, позволяя полностью перезаписать текущие значения или модифицировать их на основе предыдущих.
`typescript
// Полная замена всех параметров
searchStore.setAll({ id: '456', page: 1 });
// Модификация на основе предыдущих значений
// prev может быть undefined
searchStore.setAll((prev) => ({
...prev,
page: (prev?.page || 0) + 1,
}));
// Сохранение в истории браузера
searchStore.setAll({ id: '789' }, { replace: false });
`
Важно: setAll изменяет только те параметры, которые указаны в validationSchema.
`typescript
router.setSearchParams({ id: '1', page: 1, test: 'test' });
searchStore.setAll({ id: '2', page: 2, isShowModal: true });
router.searchParams // { id: '2', page: 2, isShowModal: true }
`
#### Удаление параметров (delete)
delete Удаляет конкретный параметр.
По-умолчанию удаление параметра не сохраняется в истории браузера (используется replace: true).
TypeScript не позволит удалить обязательные параметры - только опциональные.
`typescript
type SearchParams = {
id: string; // Обязательный - нельзя удалить
page?: number; // Опциональный - можно удалить
filters?: string[]; // Опциональный - можно удалить
};
// ✅ Можно удалить опциональные параметры
searchStore.delete('page');
searchStore.delete('filters');
// ❌ TypeScript ошибка - нельзя удалить обязательный параметр
// searchStore.delete('id'); // Error: Argument of type '"id"' is not assignable to parameter of type '"page" | "filters"'
// Сохранение в истории браузера
searchStore.delete('page', { replace: false });
`
#### Сброс значений (reset)
reset удаляет все параметры, которые указаны в generic и validationSchema.
По-умолчанию сброс не сохраняется в истории браузера (используется replace: true).
`typescript
router.setSearchParams({ id: '1', page: 1, test: 'test' });
searchStore.reset();
router.searchParams // { test: 'test' }
// Сброс с сохранением в истории браузера
searchStore.reset({ replace: false });
`
##### Частичный сброс значений
include и exclude позволяют сбросить только определенные параметры.
`typescript
searchStore.setAll({ id: '1', page: 1 });
// Сброс только id
searchStore.reset({ include: ['id'] }); // { page: 1 }
`
`typescript
searchStore.setAll({ id: '1', page: 1 });
// Сброс всех параметров, кроме id
searchStore.reset({ exclude: ['id'] }); // { id: '1' }
`
#### Создание нескольких searchParamsStore
В приложении можно создавать несколько searchParamsStore с разными validationSchema.searchParamsStore
Так как изолированы, то они не будут влиять друг на друга.
`typescript
const tableFiltersStore = router.createSearchParamsStore<{
page: number;
filters: string[];
}>({
validationSchema: {
page: v.optional(v.number()),
filters: v.optional(v.array(v.arrayItem(v.string()))),
},
});
// Если в searchParams есть modalOrgId, то открывается модалка
const detailsModalSearchParamsStore = router.createSearchParamsStore<{
modalOrgId: string;
}>({
validationSchema: {
modalOrgId: v.optional(v.string(v.guid())),
},
});
// Никак не повлияет на modalOrgId
tableFiltersStore.setAll({ page: 1, filters: ['active', 'verified'] });
detailsModalSearchParamsStore.set('modalOrgId', '123');
// Никак не повлияет на modalOrgId
tableFiltersStore.reset();
`
#### defaultValues
defaultValues применяются только при инициализации SearchParamsStore или сбросе значений.
Подробнее ниже:
##### При инициализации SearchParamsStore searchParams пустые
`typescript
router.setSearchParams({});
const searchStore = router.createSearchParamsStore
validationSchema: {
id: v.optional(v.string()),
},
defaultValues: {
id: 'default-user',
},
});
searchStore.values; // { id: 'default-user' }
router.searchParams; // { id: 'default-user' }
`
##### При инициализации SearchParamsStore searchParams частично пустые
`typescript
router.setSearchParams({ page: 2 });
const searchStore = router.createSearchParamsStore
validationSchema: {
id: v.string(),
page: v.optional(v.number()),
},
defaultValues: {
id: 'default-user',
page: 1,
},
});
// Сохранился исходный page, а для id установлено defaultValues
searchStore.values; // { id: 'default-user', page: 2 }
router.searchParams; // { id: 'default-user', page: 2 }
`
##### При инициализации SearchParamsStore валидация не прошла
`typescript
router.setSearchParams({ id: 1 });
const searchStore = router.createSearchParamsStore
validationSchema: {
id: v.string(),
},
defaultValues: {
id: 'default-user',
},
});
searchStore.values; // { id: 'default-user' }
router.searchParams; // { id: 'default-user' }
`
##### После вызова reset()
`typescript
router.setSearchParams({ id: '1' });
const searchStore = router.createSearchParamsStore
validationSchema: {
id: v.string(),
},
defaultValues: {
id: 'default-user',
},
});
searchStore.reset();
router.searchParams // { test: 'test' }
`
##### При инициализации SearchParamsStore валидация прошла, но не все optional параметры находились в исходном searchParams
`typescript
router.setSearchParams({ id: '1' });
const searchStore = router.createSearchParamsStore
validationSchema: {
id: v.string(),
page: v.optional(v.number()),
},
defaultValues: {
id: 'default-user',
page: 1,
},
});
searchStore.values; // { id: '1', page: 1 }
router.searchParams // { id: '1', page: 1 }
`
#### initialValues
initialValues - делают hard set значений в values и глобальные searchParams.
`typescript
type SearchParams = {
id: string;
};
const searchStore = router.createSearchParamsStore
validationSchema: {
id: v.string(),
},
initialValues: {
id: '1',
},
});
searchStore.values; // { id: '1' }
router.searchParams; // { id: '1' }
`
Аналогичный пример:
`typescript
type SearchParams = {
id: string;
};
const searchStore = router.createSearchParamsStore
validationSchema: {
id: v.string(),
},
});
searchStore.setAll({ id: '1' });
searchStore.values; // { id: '1' }
router.searchParams; // { id: '1' }
`
---
#### SearchParams недоступны на сервере без getServerSideProps
При обращении к router.searchParams на сервере nextjs searchParams будут {}.
Для того чтобы значение на сервере было актуальным, необходимо для конкретной страницы определить getServerSideProps:`tsx
export default () => {...}
// Достаточно просто определить функцию
export async function getServerSideProps() {
return {
props: {},
};
}
`
#### При частичной интеграции mobx-router в nextRouter.query строки записываются с кавычками
Если вы одновременно используете nextRouter.query и mobxRouter.setSearchParams, то при вызове mobxRouter.setSearchParams в nextRouter.query будут находится строки в кавычках:`js`
{
id: '"123"'
}
Для решения данной проблемы необходимо реализовывать кастомный парсер searchParams. Инструкции
---
Рекомендуется использовать типобезопасные searchParams из router.createSearchParamsStore.
router.searchParams содержит не типизированные searchParams, приведенные к js структурам.
router.searchParams в качестве значений может содержать:`ts`
type ParsedSearchParamsValue =
| string
| boolean
| number
| null
| undefined
| ParsedSearchParamsValue[]
| Record
router.searchParams является observable.
Пример использования:
`typescript
import { makeAutoObservable } from 'mobx';
class Store {
constructor(private readonly _router: Router) {
makeAutoObservable(this);
}
get currentPage() {
return this._router.searchParams.page || 1;
}
}
`
#### Установка searchParams
Метод setSearchParams позволяет изменять search параметры с сохранением текущего пути:
`typescript
router.setSearchParams({ page: 1, type: 'request' });
// Изменение с доступом к предыдущим параметрам
type PrevParams = { page: number };
router.setSearchParams((prev) => ({ page: ++(prev as PrevParams).page }));
// Установка с заменой записи в истории (по умолчанию replace: true)
router.setSearchParams({ page: 1 }, { replace: false });
`
#### Сброс searchParams
Для сброса достаточно пробросить пустой объект:
`typescript`
router.setSearchParams({});
Парсинг и сериализация параметров по-дефолту осуществляется через JSON.parse и JSON.stringify.
#### Сериализация
- Строки сериализуются с кавычками: { param: 'test' } → ?param="test"{ num: 123, bool: true }
- Числа и булевы значения преобразуются напрямую: → ?num=123&bool=true{ arr: [1,2,3] }
- Массивы и объекты сериализуются в JSON: → ?arr=[1,2,3]undefined
- Значения , null, NaN, пустые строки, пустые массивы, пустые объекты пропускаются и не добавляются в URL
#### Парсинг
- Строки в кавычках парсятся как строки: ?param="test" → { param: 'test' }?num=123
- Числа и булевы значения автоматически приводятся к соответствующим типам: → { num: 123 }?arr=[1,2,3]
- Массивы парсятся как json: → { arr: [1,2,3] }?obj={"a":2}
- Объекты парсятся как json: → { obj: { a: 2 } }"undefined"
- Строка преобразуется в undefined"null"
- Строка преобразуется в null?arr=[1,2-3]
- При ошибке парсинга значение остается строкой: → { arr: '[1,2,3]' }, ?param=str → { param: 'str' }?arr=1&arr=2
- Повторяющиеся параметры собираются в массив: → { arr: [1,2] }
#### Кастомный парсинг и сериализация searchParams
Может потребоваться изменить поведение дефолтного парсера.
Для этого необходимо реализовать class имплементирующий интерфейс ISearchParamsParser:`ts
type CustomParsedSearchParams = Record
// import { ISearchParamsParser } from '@astral/mobx-router';
interface ISearchParamsParser {
parse(rawSearchParams: URLSearchParams): CustomParsedSearchParams;
serialize(searchParams: CustomParsedSearchParams): URLSearchParams;
}
`
И передать его при создании router:`ts`
new Router(
provider,
routesConfig,
{ searchParamsParser: customParser }
);
После этого searchParams и setSearchParams будут работать с типом CustomParsedSearchParams.`ts`
router.searchParams; // Record
router.setSearchParams; // (searchParams: Record
SearchParamsStore также начнет использовать тип, заданный в парсере, и не позволит передать в generic неправильный тип:`ts`
router.createSearchParamsStore<{ id: number }>(); // Будет ошибка потому что тип должен соответствовать Record
Роутер предоставляет возможность сохранять существующие search параметры при переходах между маршрутами.
Функция полезна в сценариях, где нужно сохранить состояние фильтров, сортировок или других параметров
при навигации между разными страницами приложения.
Для сохранения параметров, необходимо передать параметр preserveOnNavigate=true и установить
значения. После редиректа, все параметры, входящие в validationSchema и установленные в URL, будут сохранены
`typescript
type SearchParams = {
id: string;
redirectUrl: string;
};
const searchStore = router.createSearchParamsStore
preserveOnNavigate: true,
validationSchema: {
id: v.string(),
redirectUrl: v.string(),
},
});
store.setAll({
id: '1',
redirectUrl: '2',
});
router.navigate('/');
router.searchParams; // { id: '1', redirectUrl: '2' }
`
Если нужно сохранять только определенные параметры - необходимо передать их ключи массивом в preserveOnNavigate:
`typescript
type SearchParams = {
id: string;
redirectUrl: string;
};
const searchStore = router.createSearchParamsStore
preserveOnNavigate: ['redirectUrl'],
validationSchema: {
id: v.string(),
redirectUrl: v.string(),
},
});
store.setAll({
id: '1',
redirectUrl: '2',
});
router.navigate('/');
router.searchParams; // { redirectUrl: '2' }
`
#### Сброс сохраненного параметра
После сброса сохраненного параметра, он более не будет сохраняться при переходах.
#### Сохранение при перезагрузке страницы
Чтобы при перезагрузке страницы preserveOnNavigate параметры продолжали сохраняться при навигации, SearchParamsStore,
инициализирующий их сохранение должен создаваться глобально.
#### Особенности реализации
Механизм сохранения search параметров основан на blocker, необходимо учитывать особенности его реализации
Если возникают проблемы установки значений - включите debug режим.
#### SearchParams устанавливаются синхронно
Методы setSearchParams, setAll, set устанавливают значения синхронно.
`typescript`
router.searchParams; // {}
router.setSearchParams({ test: 1 });
router.searchParams; // { test: 1 }
В отличии от navigate:`typescript`
router.location.pathname; // /main
router.navigate('/user');
router.location.pathname; // /user
Роутер предоставляет реактивное свойство location, которое автоматически обновляется при изменении URL в браузере.
`typescript`
type Location = {
pathname: string; // Текущий путь (например, '/dashboard')
search?: string; // Query параметры (например, '?id=123')
hash?: string; // Hash фрагмент (например, '#section')
};
Для типобезопасной навигации используйте предопределенные routes.
См. раздел Использование routes для подробной информации о навигации по routes.
#### Базовая концепция
router.createBlocker(shouldBlock) создаёт блокер. Не забудьте его активировать.
shouldBlock(nextLocation) — функция, которая возвращает true, если переход нужно остановить и false если его нужно продолжить.
Когда переход заблокирован, blocker.isBlocked === true.
Для управления блокировкой используются методы: activate, proceed, reset, destroy
#### Пример использования
`typescript
import { makeAutoObservable } from 'mobx';
import type { Router } from '#shared';
class UIStore {
private readonly blocker;
public isDirty: boolean = false;
public showModal: boolean = false;
public constructor(private readonly _router: Router) {
makeAutoObservable(this, {}, {autoBind: true});
/**
* Создаём блокер, который будет перехватывать навигацию.
* Функция обратного вызова вызывается перед каждым переходом.
* Если возвращается true — переход блокируется.
*/
this.blocker = this._router.createBlocker((nextLocation) => {
const isBlocked = this.isDirty;
if(isBlocked) {
this.openModal();
}
return isBlocked;
});
}
public mount = () => {
this.blocker.activate();
};
public unmount = () => {
this.blocker.destroy();
};
private openModal = () => {
this.showModal = true;
};
public confirm = () => {
// Продолжаем переход на заблокированный маршрут
this.blocker.proceed();
};
public cancel = () => {
// Снимаем блокировку и остаемся на текущей странице
this.blocker.reset();
};
}
const Component = observer(() => {
const [store] = useState(createUIStore);
useEffect(() => {
store.mount();
return () => {
store.unmount();
};
}, []);
});
`
React Router v6 и v7: Блокеры, созданные через createBlocker, не будут работать одновременно с хуком useBlocker.useBlocker
Если вы используете , необходимо полностью полагаться на него для управления блокировкой переходов, или использовать подход через метод createBlocker
Next.js: Для блокировки навигации используется выброс исключения throw new Error при попытке перехода на заблокированный маршрут.
Это позволяет остановить переход до того, как NextRouter выполнит смену URL.
Каждый блокер должен быть активирован перед началом работы.
`typescript`
blocker.activate();
#### Использование в компоненте
При использовании блокера в компоненте, его необходимо активировать и деактивировать в useEffect:
`tsx
useEffect(() => {
blocker.activate();
return () => {
blocker.destroy();
};
}, []);
`
Если этого не сделать, то в Strict Mode будут проблемы в работе блокера
#### Глобальный store
При использовании блокера вне компонента его можно активировать сразу в конструкторе класса:
`typescript
class GlobalStore {
constructor(private readonly _router: Router) {
this.blocker = this._router.createBlocker((nextLocation) => {
return nextLocation.pathname === '/main';
});
this.blocker.activate();
}
}
`
blocker.isBlocked является observable
`typescript
const blocker = router.createBlocker((nextLocation) => {
return nextLocation.pathname === '/main'
})
router.navigate('/main')
blocker.isBlocked // true
`
`typescript
// Активировать блокировку
blocker.activate()
// Продолжить переход на заблокированный маршрут.
blocker.proceed()
// Отменить переход и остаться на текущей странице.
blocker.reset()
// Удалить блокер
blocker.destroy()
`
Можно создавать несколько блокеров одновременно. Навигация будет заблокирована, если хотя бы один из активных блокеров запретит переход на другую страницу.
Каждый блокер имеет свое независимое состояние isBlocked
`typescript
const blockMain = router.createBlocker((next) => next.pathname === '/main');
blockMain.activate();
const blockUsers = router.createBlocker((next) => next.pathname === '/users');
blockUsers.activate();
router.navigate('/users'); // переход заблокирован
router.navigate('/settings'); // переход разрешён
`
Каждый блокер подписывается на события изменения маршрута. Необходимо удалять его, если он больше не используется.
Если не вызывать destroy, блокер продолжит слушать изменения маршрутов, что может привести к непредсказуемому поведению в навигации.
`typescript
const blocker = router.createBlocker((nextLocation) => nextLocation.pathname === '/main');
blocker.activate();
blocker.destroy(); // снимает все подписки и очищает внутренние ссылки
`
Функция matchPath позволяет получить детальную информацию о текущем маршруте и извлечь параметры из URL. Это мощный инструмент для анализа URL и получения метаданных о совпадении паттерна маршрута с текущим путем.
#### Синтаксис
`typescript`
router.matchPath(pattern: MatchPathPattern | string, path: string): MatchPathInfo | null
#### Параметры
- pattern - Паттерн маршрута (строка или объект MatchPathPattern)isExact: true
- При передаче строки используется точное сопоставление ( по умолчанию)MatchPathPattern
- При передаче объекта можно указать isExact: false для частичного сопоставленияpath
- - URL pathname для сопоставления
#### Возвращаемое значение
`typescript`
type MatchPathInfo = {
params: Record
pathname: string; // Полный совпадающий путь
pathnameBase: string; // Базовый путь для дочерних маршрутов
pattern: string; // Исходный паттерн маршрута
};
Самый простой способ использования - сопоставление строкового паттерна с URL:
`typescript
// Сопоставление простого маршрута
const match = router.matchPath('/user/:id', '/user/123');
// {
// params: { id: '123' },
// pathname: '/user/123',
// pattern: '/user/:id',
// pathnameBase: '/user/123'
// }
// Сопоставление с несколькими параметрами
const postMatch = router.matchPath('/posts/:category/:id', '/posts/tech/456');
// {
// params: { category: 'tech', id: '456' },
// pathname: '/posts/tech/456',
// pattern: '/posts/:category/:id',
// pathnameBase: '/posts/tech/456'
// }
// Когда сопоставления нет - возвращается null
const noMatch = router.matchPath('/user/:id', '/posts/123');
// null
`
По умолчанию isExact равен true, что означает точное сопоставление пути.
Для более точного контроля над сопоставлением используйте объект MatchPathPattern:
`typescript
// Точное сопоставление (значение по умолчанию)
const exactMatch = router.matchPath(
{ path: '/user/:id' },
'/user/123'
);
// {
// params: { id: '123' },
// pathname: '/user/123',
// pattern: '/user/:id',
// pathnameBase: '/user/123'
// }
// Частичное сопоставление (isExact: false)
const partialMatch = router.matchPath(
{ path: '/user', isExact: false },
'/user/123/settings'
);
// {
// params: {},
// pathname: '/user/123/settings',
// pattern: '/user',
// pathnameBase: '/user'
// }
`
`typescript`
// Сопоставит любые пути, начинающиеся с '/posts/'
const wildcardMatch = router.matchPath(
{ path: '/posts/*' },
'/posts/tech/123/comments'
);
// {
// params: {},
// pathname: '/posts/tech/123/comments',
// pattern: '/posts/*',
// pathnameBase: '/posts'
// }
router.location - это MobX observable.
`typescript`
type Location = {
pathname: string; // Текущий путь (например, '/dashboard')
search?: string; // Query параметры (например, '?id=123&tab=settings')
hash?: string; // Hash фрагмент (например, '#section')
state?: NavigationState;
};
Основное свойство pathname содержит текущий путь URL без query параметров и hash фрагмента.
#### Примеры использования
`typescript`
// Получение текущего пути
const currentPath = router.location.pathname;
console.log(currentPath); // '/user/123'
#### Реактивное использование с MobX
`typescript
import { makeAutoObservable } from 'mobx';
class NavigationStore {
constructor(private readonly _router: Router) {
makeAutoObservable(this);
}
// Computed свойства на основе pathname
get isAdminPage() {
return this._router.location.pathname.startsWith('/admin');
}
}
`
Методы subscribeRouteChangeStart и subscribeRouteChangeEnd позволяют подписываться на начало и окончание навигации.
#### Особенности реализации
- React-Router v6 и v7: На данный момент не поддерживается метод subscribeRouteChangeStart
- Начало навигации (subscribeRouteChangeStart) — вызывается сразу после инициирования перехода на новый маршрут, до фактической активации/рендера компонентов.
- Окончание навигации (subscribeRouteChangeEnd) — вызывается после успешного завершения перехода и рендера нового маршрута.
#### Синтаксис
`typescript`
const disposeStart = router.subscribeRouteChangeStart(handler: (location: Location) => void): () => void;
const disposeEnd = router.subscribeRouteChangeEnd(handler: (location: Location) => void): () => void;
#### Параметры
- handler - функция-обработчик, которая будет вызвана с объектом Location или строкой url:subscribeRouteChangeStart
- для — при начале навигацииsubscribeRouteChangeEnd
- для — после завершения навигации
#### Возвращаемое значение
Функция subscribeRouteChangeStart и subscribeRouteChangeEnd возвращает функцию dispose, которая отписывает обработчик от событий.
#### Примеры использования
`typescript
// Подписка на начало перехода:
const disposeStart = router.subscribeRouteChangeStart((location) => {
console.log('Начинается переход на:', location.pathname);
});
// Отписка от события перехода:
disposeStart();
// Подписка на окончание перехода:
const disposeEnd = router.subscribeRouteChangeEnd((location) => {
console.log('Переход завершён:', location.pathname);
});
// Отписка от события перехода:
disposeEnd();
`
В debug режиме router будет логировать внутренние операции.
`typescript`
const router = new MobxRouter(
provider,
routesConfig,
{ logLevel: 'debug' },
);
Для мока роутера используйте createRouterMockFactory.
RouterMock полностью эмулирует поведение реального роутера в памяти, сохраняя реактивность и особенности поведения (например, асинхронный navigate).
RouterMock создается на основе реального роутера, что позволяет переиспользовать его конфигурацию и маршруты.
`shared/services/Router``typescript
import { MobxRouter } from '@astral/mobx-router';
import { ReactRouterProvider } from '@astral/mobx-router-react';
export const reactRouterProvider = new ReactRouterProvider();
const routesConfig = {
home: MobxRouter.defineRoute({
pattern: '/home',
}),
user: MobxRouter.defineRoute({
pattern: '/user/:id',
generatePath: (params: { id: string }) => /user/${params.id},/profile/${userId}
}),
profile: MobxRouter.defineRoute({
pattern: '/profile/:userId?',
generatePath: ({ userId }: { userId?: string }) => userId ? : '/profile' ,
}),
};
export const router = new MobxRouter(reactRouterProvider, routesConfig);
`
`shared/_test/RouterMock``typescript
import { createRouterMockFactory } from '@astral/mobx-router';
import { router } from '#shared/services/Router';
export const createRouterMock = createRouterMockFactory(router);
`
`typescript`
type Params = {
initialLocation?: Location;
initialSearchParams?: SearchParams;
};
#### Вспомогательные методы
##### waitNavigation
Ожидает завершения навигации.
`typescript
it('После успешного submit происходит переход на страницу с организацией', async () => {
const routerMock = createRouterMock();
const sut = createStore(routerMock);
await sut.submit();
// Navigate - асинхронная операция. Нужно дождаться её завершения
await routerMock.waitNavigation();
expect(sut.location.pathname).toBe(routerMock.routes.org.generateHref());
});
`
#### Базовый пример
`typescript
it('Удаляется организация, если активен home route', () => {
const routerMock = createRouterMock({ initialLocation: { pathname: '/home' } });
const sut = createStore(routerMock);
expect(sut.orgId).toBeUndefined();
});
`
#### Работа с навигацией
`typescript
it('После успешного submit происходит переход на страницу с организацией', async () => {
const routerMock = createRouterMock();
const sut = createStore(routerMock);
await sut.submit();
// Navigate - асинхронная операция. Нужно дождаться её завершения
await routerMock.waitNavigation();
expect(sut.location.pathname).toBe(routerMock.routes.org.generateHref());
});
`
#### Работа с searchParams
`typescript
it('Filters содержит начальные значения из url', async () => {
const routerMock = createRouterMock({
initialSearchParams: {
page: 1,
limit: 10,
},
});
const sut = createStore(routerMock);
expect(sut.filters).toEqual({
page: 1,
limit: 10,
});
});
`
`typescript
it('Фильтры удаляются из url после submit', async () => {
const routerMock = createRouterMock({
initialSearchParams: {
page: 1,
limit: 10,
},
});
const sut = createStore(routerMock);
await sut.submit();
expect(routerMock.searchParams).toEqual({});
});
`
#### Работа с routeParams
`typescript
...
const routesConfig = {
user: MobxRouter.defineRoute({
pattern: '/org/:id',
generatePath: (params: { id: string }) => /org/${params.id},
}),
};
...
`
`typescript
it('orgId содержит значение из url', () => {
const routerMock = createRouterMock({ initialLocation: { pathname: '/org/123' } });
const sut = createStore(routerMock);
expect(sut.orgId).toBe('123');
});
``