<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>

# @astral/mobx-router

Абстрактный реактивный router service, позволяющий использовать один интерфейс взаимодействия для разных роутеров:
- react-router v6 и v7
- nextjs router

## Краткий обзор

<div align="center">
  <a href="https://www.youtube.com/watch?v=a8Bh92baVmI">
    <img src="https://storage.yandexcloud.net/astral-frontend/mobx-router-record.png" style="max-width: 650px">
  </a>
</div>

## Содержание

- [Getting Started](#getting-started)
  - [React Router v6/v7](#react-router-v6v7)
  - [Next.js (Pages Router)](#nextjs-pages-router)
  - [Использование](#использование)
- [Определение routes](#определение-routes)
  - [Навигация (`navigate`)](#навигация-navigate)
  - [Генерация ссылок](#генерация-ссылок)
- [Базовый путь приложения (baseName)](#базовый-путь-приложения-basename)
  <!-- cspell:ignore: routeparams -->
- [RouteParams](#routeparams)
- [Навигация](#навигация)
  - [Типобезопасная навигация](#типобезопасная-навигация)
  - [Метод `navigate`](#метод-navigate)
    - [Асинхронный переход `navigateAsync`](#асинхронный-переход-navigateasync)
    - [Возврат по истории назад (back)](#возврат-по-истории-назад-back)
    - [Перезагрузка страницы (reload)](#перезагрузка-страницы-reload)
    - [State при навигации](#state-при-навигации)
- [Работа с SearchParams](#работа-с-searchparams)
    - [Типобезопасные изолированные searchParams](#типобезопасные-изолированные-searchparams)
      - [Values](#values)
      - [Валидация](#валидация)
        - [Валидация дат](#валидация-дат)
      <!-- cspell:ignore: setall -->
      - [Установка значений (set, setAll)](#установка-значений-set-setall)
      - [Удаление параметров (delete)](#удаление-параметров-delete)
      - [Сброс значений (reset)](#сброс-значений-reset)
      <!-- cspell:ignore: searchparamsstore -->
      - [Создание нескольких searchParamsStore](#создание-нескольких-searchparamsstore)
      <!-- cspell:ignore: initialvalues -->
      - [initialValues](#initialvalues)
      <!-- cspell:ignore: defaultvalues -->
      - [defaultValues](#defaultvalues)
  - [Особенности работы с nextjs](#особенности-работы-с-nextjs)
  - [Не типизированные searchParams, приведенные к js структурам](#не-типизированные-searchparams-приведенные-к-js-структурам)
  - [Парсинг и сериализация searchParams](#парсинг-и-сериализация-searchparams)
  - [Кастомный парсинг и сериализация searchParams](#кастомный-парсинг-и-сериализация-searchparams)
  - [Сохранение Search параметров при навигации](#сохранение-search-параметров-при-навигации)
- [Блокировка маршрутов (blocker)](#блокировка-маршрутов-blocker)
  - [Состояние блокировки](#состояние-блокировки)
  - [Управления блокировкой](#управления-блокировкой)
  - [Работа с несколькими блокерами](#работа-с-несколькими-блокерами)
  - [Управление жизненным циклом (destroy)](#управление-жизненным-циклом-destroy)
- [MatchPath - Сопоставление маршрутов](#matchpath---сопоставление-маршрутов)
  - [API](#api)
  - [Базовый пример](#базовый-пример)
  - [Полное сопоставление](#полное-сопоставление)
  - [Практические примеры](#практические-примеры)
- [Location](#location)
  - [API](#api-1)
  - [pathname](#pathname)
- [SubscribeRoute](#подписка-на-события-маршрутизации)
- [Debug](#debug)
- [Тестирование](#тестирование)

## Getting Started

### React Router v6/v7

#### Установка

```bash
npm install @astral/mobx-router @astral/mobx-router-react mobx react-router
```

#### Подготовка

- Для работы с react-router v6/v7 **необходимо использовать [`DataRouter`](https://reactrouter.com/start/data/installation)**. Ниже описан пример настройки

- Все импорты должны быть из `react-router`, а не из `react-router-dom`

```typescript
// ✅
import { createBrowserRouter, useLocation } from 'react-router';

// ❌
import { createBrowserRouter, useLocation } from 'react-router-dom';
```

#### Настройка

**Важно**: Для работы с react-router v6/v7 необходимо использовать [`DataRouter`](https://reactrouter.com/start/data/installation).

```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: MobxRouter.defineRoute({
    pattern: '/profile/:userId?',
    generatePath: ({ userId }: { userId?: string }) => userId ? `/profile/${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: (
      <>
        <ProviderAdapter provider={reactRouterProvider} />
        <MainLayout>
          <Suspense
            fallback={<ContentState isLoading>loading...</ContentState>}
          >
            <Outlet />
          </Suspense>
        </MainLayout>
      </>
    ),
    children: [
      {
        path: '/',
        element: <IndexPage />,
      },
      {
        path: router.routes.home.pattern,
        element: <HomePage />,
      },
      {
        path: router.routes.user.pattern,
        element: <UserPage />,
      },
      {
        path: router.routes.profile.pattern,
        element: <ProfilePage />,
      },
    ],
  },
]);
```

```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 (
    <ReactRouterProvider router={browserRouter} />
  );
};
```

### Next.js (Pages Router)

**Поддерживает только 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: MobxRouter.defineRoute({
    pattern: '/profile/[[userId]]',
    generatePath: ({ userId }: { userId?: string }) => userId ? `/profile/${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 (
    <>
      <ProviderAdapter provider={provider} />
      <Component {...pageProps} />
    </>
  );
}

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');
  }
}
```

## Определение routes

После создания роутера необходимо определить маршруты приложения:
- Для статических маршрутов достаточно указать только `pattern`. Он будет использоваться как pathname для переходов
- Для динамических маршрутов с использованием routeParams необходимо определять `generatePath`. Если `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: MobxRouter.defineRoute({
    pattern: '/profile/:userId?',
    generatePath: ({ userId }: { userId?: string }) => userId ? `/profile/${userId}` : '/profile' ,
  }),
};

// Создаем роутер с конфигурацией
export const router = new MobxRouter(provider, routesConfig);
```

### Навигация (`navigate`)

Интерфейс `navigate` аналогичен интерфейсу корневого [`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

Каждый маршрут также предоставляет доступ к своим параметрам через свойство `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 предоставляет свойство `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;
  }
}
```

### Генерация ссылок (`generateHref`)

Для создания ссылок на route используйте метод `generateHref`:

```typescript
const profileLink = router.routes.profile.generateHref({
  userId: '456',
}); // /profile/456

// Использование в компонентах
<Link to={profileLink}>Мой профиль</Link>
```

#### Генерация ссылки с searchParams

```typescript
const userLink = router.routes.user.generateHref(
  { id: '123' },
  { searchParams: { page: 1 } },
); // /user/123?page=1

// Использование в компонентах
<Link to={userLink}>Мой профиль</Link>;
```

## Базовый путь приложения (baseName)

Для настройки базового пути приложения используйте опцию `baseName` при создании MobxRouter.

⚠️ В Next.js Router этот параметр **не поддерживается**

```typescript
const browserRouter = createBrowserRouter(routes, {
   basename: '/app',
});

const router = new MobxRouter(reactRouterProvider, routesConfig, {
  // Должен совпадать с basename, переданным в React Router
  baseName: '/app',
});
```

## RouteParams

Роутер предоставляет доступ к параметрам текущего активного маршрута через свойство `routeParams`:

```typescript
const routesConfig = {
  user: Router.defineRoute({
    pattern: '/user/:id',
    generatePath: (params: { id: string }) => `/user/${params.id}`,
  }),
};

const router = new Router(provider, routesConfig);

// Получение параметров текущего активного маршрута
console.log(router.routeParams); // { id: '123' }
```

## Навигация

### Метод `navigate`

Основной метод для навигации между страницами приложения.

#### Синтаксис

```typescript
router.navigate(to: string | Location, options?: NavigateOptions)
```

#### Параметры

- `to` (string | Location) - путь для перехода (строка или объект)
- `options` (NavigateOptions, опционально) - дополнительные параметры навигации

#### Тип NavigateOptions

```typescript
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;
  /**
   * Дополнительные данные, которые будут переданы в `location.state`
   * при навигации (поддерживается только в 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` можно ознакомиться [здесь](https://developer.mozilla.org/en-US/docs/Web/API/History/state)

##### Особенности реализации

- **NextRouter**: параметр `state` не поддерживается

##### Тип NavigationState

```typescript
type NavigationState = {
  /**
   * Информация о предыдущем location, с которого был совершен переход
   */
  referer: {
    /**
     * Путь предыдущего location
     */
    pathname: string;
    /**
     * search предыдущего location
     */
    search?: string;
  };
} & Record<string, unknown>;
```

##### Пример использования

```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' },
// }
```

## Работа с SearchParams

Роутер предоставляет несколько способов работы с search параметрами URL.

### Типобезопасные изолированные searchParams

#### Базовая концепция

- **Типобезопасность**: `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<SearchParams>({
            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.

#### Валидация

Валидация происходит:
- При инициализации `SearchParamsStore`
- При обращении к `values` или `validationErrors`
- При изменении `searchParams` в рамках текущего store или в глобальном `router.searchParams`

`store.values` содержат:
- `undefined` если валидация завершилась ошибкой
- значения после парсинга если валидация завершилась без ошибок

`validationSchema` принимает [схему объекта для @astral/validations](https://www.npmjs.com/package/@astral/validations#basic-usage).

<!-- llm-ignore:enable -->

##### Тип SearchParamsValidationSchema

```typescript
// Schema - схема объекта из @astral/validations
type SearchParamsValidationSchema<TSearchParams> = Schema<TSearchParams>;
```

Если схему нужно вынести из вызова `createSearchParamsStore` в отдельную переменную, константу или поле класса,
то для ее типизации используется `SearchParamsValidationSchema`:

```typescript
import * as v from '@astral/validations';
import type { SearchParamsValidationSchema } from '@astral/mobx-router';

type SearchParams = {
  id: string;
  page?: number;
};

const searchParamsSchema: SearchParamsValidationSchema<SearchParams> = {
  id: v.string(),
  page: v.optional(v.number()),
};

const searchStore = router.createSearchParamsStore<SearchParams>({
  validationSchema: searchParamsSchema,
});
```

В схеме должны быть описаны **все** параметры, включая опциональные: для `page?: number` в схеме
все равно требуется ключ `page` со значением `v.optional(...)`.

<!-- llm-ignore:disable -->

##### Валидация при инициализации `SearchParamsStore`

Если валидация при инициализации `SearchParamsStore` завершилась ошибкой, то `values` будут содержать `undefined`, **при этом `router.searchParams` не изменится** если не заданы [`defaultValues`](#defaultvalues).

##### Валидация при обращении к `values`

Валидация при обращении к values никак не меняет `router.searchParams`.

##### Валидация дат

Для валидации дат необходимо использовать метод [transform](https://www.npmjs.com/package/@astral/validations#transform)

```typescript
import * as v from '@astral/validations';

type SearchParams = {
  date: string;
};

const searchStore = router.createSearchParamsStore<SearchParams>({
    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<SearchParams>({
    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<SearchParams>({
  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<SearchParams>({
  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<SearchParams>({
  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<SearchParams>({
  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<SearchParams>({
  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 }
```

##### defaultValues как функция. Динамическая установка defaultValues 

`defaultValues` может быть функцией `(values: ParsedSearchParams<TSearchParamsParser>) => TSearchParams`.

```typescript
type SearchParams = {
  dateFrom?: string;
  dateTo?: string;
};

const searchStore = router.createSearchParamsStore<SearchParams>({
  validationSchema: {
    dateFrom: v.optional(v.string(v.transform((value) => new Date(value)))),
    dateTo: v.optional(v.string(v.transform((value) => new Date(value)))),
  },
  defaultValues: (values) => {
    // Содержит сырые и не провалидированные values
    // Если в searchParams уже есть хотя бы одна дата — не подставляем defaultValues
    if (values?.fromDate || values?.toDate) {
      return values;
    }
    
    return {
      dateFrom: this.defaultDateRange.start,
      dateTo: this.defaultDateRange.end,
    };
  },
});
```

#### initialValues

`initialValues` - делают hard set значений в values и глобальные searchParams.

```typescript
type SearchParams = {
  id: string;
};

const searchStore = router.createSearchParamsStore<SearchParams>({
  validationSchema: {
    id: v.string(),
  },
  initialValues: {
    id: '1',
  },
});

searchStore.values; // { id: '1' }
router.searchParams; // { id: '1' }
```

Аналогичный пример:
```typescript
type SearchParams = {
  id: string;
};

const searchStore = router.createSearchParamsStore<SearchParams>({
  validationSchema: {
    id: v.string(),
  },
});

searchStore.setAll({ id: '1' });

searchStore.values; // { id: '1' }
router.searchParams; // { id: '1' }
```

---

### Особенности работы с nextjs

#### 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)

---

### Не типизированные searchParams, приведенные к js структурам

**Рекомендуется использовать [типобезопасные searchParams из `router.createSearchParamsStore`](#типобезопасные-изолированные-searchparams)**.

`router.searchParams` содержит не типизированные searchParams, приведенные к js структурам.

`router.searchParams` в качестве значений может содержать:
```ts
type ParsedSearchParamsValue =
  | string
  | boolean
  | number
  | null
  | undefined
  | ParsedSearchParamsValue[]
  | Record<string, ParsedSearchParamsValue>;
```

`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({});
```

### Парсинг и сериализация searchParams

Парсинг и сериализация параметров **по-дефолту** осуществляется через `JSON.parse` и `JSON.stringify`.

#### Сериализация 

- Строки сериализуются с кавычками: `{ param: 'test' }` → `?param="test"`
- Числа и булевы значения преобразуются напрямую: `{ num: 123, bool: true }` → `?num=123&bool=true`
- Массивы и объекты сериализуются в JSON: `{ arr: [1,2,3] }` → `?arr=[1,2,3]`
- Значения `undefined`, `null`, `NaN`, пустые строки, пустые массивы, пустые объекты пропускаются и не добавляются в URL

#### Парсинг

- Строки в кавычках парсятся как строки: `?param="test"` → `{ param: 'test' }`
- Числа и булевы значения автоматически приводятся к соответствующим типам: `?num=123` → `{ num: 123 }`
- Массивы парсятся как json: `?arr=[1,2,3]` → `{ arr: [1,2,3] }`
- Объекты парсятся как json: `?obj={"a":2}` → `{ 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<string, string>;

// 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<string, string>
router.setSearchParams; // (searchParams: Record<string, string>) => void 
```

`SearchParamsStore` также начнет использовать тип, заданный в парсере, и не позволит передать в generic неправильный тип:
```ts
router.createSearchParamsStore<{ id: number }>(); // Будет ошибка потому что тип должен соответствовать Record<string, string>
```

### Сохранение Search параметров при навигации

Роутер предоставляет возможность сохранять существующие search параметры при переходах между маршрутами. 
Функция полезна в сценариях, где нужно сохранить состояние фильтров, сортировок или других параметров
при навигации между разными страницами приложения.

Для сохранения параметров, необходимо передать параметр preserveOnNavigate=true и [установить](#установка-значений-set-setall)
значения. После редиректа, все параметры, входящие в validationSchema и установленные в URL, будут сохранены 

```typescript
type SearchParams = {
    id: string;
    redirectUrl: string;
};

const searchStore = router.createSearchParamsStore<SearchParams>({
    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<SearchParams>({
    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

Если возникают проблемы установки значений - включите [debug режим](#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

### Свойство `location`

Роутер предоставляет реактивное свойство `location`, которое автоматически обновляется при изменении URL в браузере.

```typescript
type Location = {
  pathname: string;  // Текущий путь (например, '/dashboard')
  search?: string;   // Query параметры (например, '?id=123')
  hash?: string;     // Hash фрагмент (например, '#section')
};
```

### Типобезопасная навигация

Для типобезопасной навигации используйте предопределенные routes.

См. раздел [Использование routes](#использование-routes) для подробной информации о навигации по routes.

## Блокировка маршрутов (blocker)

#### Базовая концепция

`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)

Каждый блокер подписывается на события изменения маршрута. Необходимо удалять его, если он больше не используется.

Если не вызывать `destroy`, блокер продолжит слушать изменения маршрутов, что может привести к непредсказуемому поведению в навигации.

```typescript
const blocker = router.createBlocker((nextLocation) => nextLocation.pathname === '/main');

blocker.activate();

blocker.destroy(); // снимает все подписки и очищает внутренние ссылки
```

## MatchPath - Сопоставление маршрутов

Функция `matchPath` позволяет получить детальную информацию о текущем маршруте и извлечь параметры из URL. Это мощный инструмент для анализа URL и получения метаданных о совпадении паттерна маршрута с текущим путем.

### API

#### Синтаксис

```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<string, string>;    // Динамические параметры из URL
  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`

По умолчанию `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'
// }
```

### Сопоставление с wildcard

```typescript
// Сопоставит любые пути, начинающиеся с '/posts/'
const wildcardMatch = router.matchPath(
  { path: '/posts/*' }, 
  '/posts/tech/123/comments'
);
// {
//   params: {},
//   pathname: '/posts/tech/123/comments',
//   pattern: '/posts/*',
//   pathnameBase: '/posts'
// }
```

## Location

`router.location` - это MobX observable.

### API

```typescript
type Location = {
  pathname: string;  // Текущий путь (например, '/dashboard')
  search?: string;   // Query параметры (например, '?id=123&tab=settings')
  hash?: string;     // Hash фрагмент (например, '#section')
  state?: NavigationState; 
};
```

### pathname

Основное свойство `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` позволяют подписываться на начало и окончание навигации.

#### Особенности реализации

- Начало навигации (subscribeRouteChangeStart) — вызывается сразу после инициирования перехода на новый маршрут, до фактической активации/рендера компонентов.
- Окончание навигации (subscribeRouteChangeEnd) — вызывается после успешного завершения перехода и рендера нового маршрута.

### API

#### Синтаксис

```typescript
const disposeStart = router.subscribeRouteChangeStart(handler: (location: Location) => void): () => void;
const disposeEnd = router.subscribeRouteChangeEnd(handler: (location: Location) => void): () => void;
```

#### Параметры

- `handler` - функция-обработчик, которая будет вызвана с объектом [Location](#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

В debug режиме router будет логировать внутренние операции.

```typescript
const router = new MobxRouter(
  provider,
  routesConfig,
  { logLevel: 'debug' },
);
```

## Тестирование

<!-- llm-section-id: mobx-router:tests -->

Для мока роутера используйте `createRouterMockFactory`.

`RouterMock` полностью эмулирует поведение реального роутера в памяти, сохраняя реактивность и особенности поведения (например, асинхронный `navigate`).

`RouterMock` создается на основе реального роутера, что позволяет переиспользовать его конфигурацию и маршруты.

### Фабрика `createRouterMock` (задается один раз)

```shared/services/Router/Router.ts```
```typescript
import { MobxRouter } from '@astral/mobx-router';

const routesConfig = {
  home: MobxRouter.defineRoute({
    pattern: '/home',
  }),
};

export const router = new MobxRouter(reactRouterProvider, routesConfig);
```

```shared/tests/RouterMock/RouterMock.ts```
```typescript
import { createRouterMockFactory } from '@astral/mobx-router';

import { router } from '#shared/services/Router';

export const createRouterMock = createRouterMockFactory(router);
```

### Использование `routerMock`

#### Для каждого теста свой routerMock instance

```ts
const setup = () => {
  const routerMock = createRouterMock();

  const sut = new Store(routerMock);
};
```

#### Проверка состояния location, searchParams, routeParams

```ts
expect(routerMock.location.pathname).toBe('/main');

expect(routerMock.searchParams).toBeEqual({ page: 1 });
```

#### Initial path, searchParams, routeParams

```ts
createRouterMock({
  initialLocation: { pathname: '/main/:userId' },
  searchParams: { page: 1 }
});
```

#### Ожидание завершения navigate

```ts
await routerMock.waitNavigation();
```

#### Изменение location

```ts
await routerMock.navigateAsync('/main/123')
```

#### Изменение searchParams

```ts
router.setSearchParams({ page: 1 });
```

#### SearchParamsStore

`SearchParamsStore` получает состояние из `router.searchParams`: установка значений через `router.setSearchParams` установит `values` и в `SearchParamsStore`.
