npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@astral/mobx-router

v1.8.1

Published

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

Readme

@astral/mobx-router

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

  • react-router v6 и v7
  • nextjs router

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

Содержание

Getting Started

React Router v6/v7

Установка

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

Подготовка

  • Для работы с react-router v6/v7 необходимо использовать DataRouter. Ниже описан пример настройки

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

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

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

Настройка

Важно: Для работы с react-router v6/v7 необходимо использовать DataRouter.

shared/services/Router

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

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

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

Установка

npm install @astral/mobx-router @astral/mobx-router-nextjs-pages mobx

Настройка

shared/services/Router

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

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;

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

После настройки вы можете использовать роутер для навигации:

// Простой переход
router.navigate('/dashboard');

// Переход с заменой истории
router.navigate('/login', { replace: true });

Или подписываться на изменения роутера в mobx:

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 не будет принимать параметров, возникнет исключение
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.

После определения routes вы можете использовать их для типобезопасной навигации:

// Простая навигация без параметров
router.routes.home.navigate();

// Навигация с обязательными параметрами
router.routes.user.navigate({ params: { id: '123' } });

// Навигация с опциональными параметрами
router.routes.profile.navigate(); // без параметров
router.routes.profile.navigate({ params: { userId } }); // с параметрами

RouteParams

Каждый маршрут также предоставляет доступ к своим параметрам через свойство routeParams:

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 будет соответствовать типу:

{ id: string } | undefined

undefined, если маршрут не активен.

Проверка активности route (isActive)

Каждый route предоставляет свойство isActive, которое позволяет определить, является ли данный route активным в текущий момент. Это свойство является MobX observable и автоматически обновляется при изменении URL.

Синтаксис

router.routes.routeName.isActive: boolean

Пример

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);
...
import { makeAutoObservable } from 'mobx';

class SidebarStore {
  constructor(private readonly _router: Router) {
    makeAutoObservable(this);
  }

  get isUserRouteActive() {
    return this._router.routes.user.isActive;
  }
}

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

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

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

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

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

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

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

RouteParams

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

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

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

Синтаксис

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

Параметры

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

Тип NavigateOptions

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

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

// Простой переход на страницу (строка)
router.navigate('/main');

// Переход с заменой текущей записи в истории браузера
router.navigate('/main', { replace: true });

// Переход с отключением автоматической прокрутки
router.navigate('/main', { scrollRestoration: 'manual' });

// Комбинирование параметров
router.navigate('/main', {                                                                                                                                                                                                                                                                                                                                              
  replace: true, 
  scrollRestoration: 'manual' 
});

Переход с установкой searchParams

router.navigate({
  pathname: '/user/123',
  // ?page=1&filters=[1,2]
  searchParams: { page: 1, filters: [1, 2] }
});

Передача search строки

Search можно передавать как ?, так и без:

// Переход с search параметрами (с ?)
router.navigate({
  pathname: '/search',
  search: '?query=test&page=1'
});

// Переход с search параметрами (без ?)
router.navigate({
  pathname: '/search',
  search: 'query=test&page=1'
});

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

  • navigate не синхронный:
router.location.pathname; // /main
router.navigate('/user');
router.location.pathname; // /user

Асинхронный переход navigateAsync

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

Завершение навигации - успешный рендеринг нового маршрута.

// Переход на страницу и ожидание завершения
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 в истории

router.navigate('/user'); // /main -> /user

router.back(); // возврат на /main

Перезагрузка страницы (reload)

Метод перезагружает текущую страницу

router.reload();

state при навигации

О работе state можно ознакомиться здесь

Особенности реализации
  • NextRouter: параметр state не поддерживается
Тип NavigationState
type NavigationState = {
  /**
   * Информация о предыдущем location, с которого был совершен переход
   */
  referer: {
    /**
     * Путь предыдущего location
     */
    pathname: string;
    /**
     * search предыдущего location
     */
    search?: string;
  };
} & Record<string, unknown>;
Пример использования
// Навигация с передачей 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.

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

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.

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

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

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

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

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

Для валидации дат необходимо использовать метод transform

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 объект с информацией об ошибке:

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

Устанавливает значение для конкретного параметра, сохраняя остальные параметры без изменений.

// Установка обязательного параметра
searchStore.set('id', '123');

// Установка массива
searchStore.set('filters', ['active', 'verified']);

// Сохранение в истории браузера
searchStore.set('page', 3, { replace: false });
setAll

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

// Полная замена всех параметров
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.

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 не позволит удалить обязательные параметры - только опциональные.

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

router.setSearchParams({ id: '1', page: 1, test: 'test' });

searchStore.reset();

router.searchParams // { test: 'test' }

// Сброс с сохранением в истории браузера
searchStore.reset({ replace: false });
Частичный сброс значений

include и exclude позволяют сбросить только определенные параметры.

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

// Сброс только id
searchStore.reset({ include: ['id'] }); // { page: 1 }
searchStore.setAll({ id: '1', page: 1 });

// Сброс всех параметров, кроме id
searchStore.reset({ exclude: ['id'] }); // { id: '1' }

Создание нескольких searchParamsStore

В приложении можно создавать несколько searchParamsStore с разными validationSchema. Так как searchParamsStore изолированы, то они не будут влиять друг на друга.

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 пустые
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 частично пустые
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 валидация не прошла
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()
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
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 }

initialValues

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

type SearchParams = {
  id: string;
};

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

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

Аналогичный пример:

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:

export default () => {...}

// Достаточно просто определить функцию
export async function getServerSideProps() {
	return {
		props: {},
	};
}

При частичной интеграции mobx-router в nextRouter.query строки записываются с кавычками

Если вы одновременно используете nextRouter.query и mobxRouter.setSearchParams, то при вызове mobxRouter.setSearchParams в nextRouter.query будут находится строки в кавычках:

{
  id: '"123"'
}

Для решения данной проблемы необходимо реализовывать кастомный парсер searchParams. Инструкции


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

Рекомендуется использовать типобезопасные searchParams из router.createSearchParamsStore.

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

router.searchParams в качестве значений может содержать:

type ParsedSearchParamsValue =
  | string
  | boolean
  | number
  | null
  | undefined
  | ParsedSearchParamsValue[]
  | Record<string, ParsedSearchParamsValue>;

router.searchParams является observable.

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

import { makeAutoObservable } from 'mobx';

class Store {
  constructor(private readonly _router: Router) {
    makeAutoObservable(this);
  }

  get currentPage() {
    return this._router.searchParams.page || 1;
  }
}

Установка searchParams

Метод setSearchParams позволяет изменять search параметры с сохранением текущего пути:

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

Для сброса достаточно пробросить пустой объект:

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:

type CustomParsedSearchParams = Record<string, string>;

// import { ISearchParamsParser } from '@astral/mobx-router';
interface ISearchParamsParser {
  parse(rawSearchParams: URLSearchParams): CustomParsedSearchParams;
  serialize(searchParams: CustomParsedSearchParams): URLSearchParams;
}

И передать его при создании router:

new Router(
  provider,
  routesConfig,
  { searchParamsParser: customParser }
);

После этого searchParams и setSearchParams будут работать с типом CustomParsedSearchParams.

router.searchParams; // Record<string, string>
router.setSearchParams; // (searchParams: Record<string, string>) => void 

SearchParamsStore также начнет использовать тип, заданный в парсере, и не позволит передать в generic неправильный тип:

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

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

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

Для сохранения параметров, необходимо передать параметр preserveOnNavigate=true и установить значения. После редиректа, все параметры, входящие в validationSchema и установленные в URL, будут сохранены

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:

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 режим.

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

SearchParams устанавливаются синхронно

Методы setSearchParams, setAll, set устанавливают значения синхронно.

router.searchParams; // {}
router.setSearchParams({ test: 1 });
router.searchParams; // { test: 1 }

В отличии от navigate:

router.location.pathname; // /main
router.navigate('/user');
router.location.pathname; // /user

Location

Свойство location

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

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

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

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

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

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

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

router.createBlocker(shouldBlock) создаёт блокер. Не забудьте его активировать.

shouldBlock(nextLocation) — функция, которая возвращает true, если переход нужно остановить и false если его нужно продолжить.

Когда переход заблокирован, blocker.isBlocked === true.

Для управления блокировкой используются методы: activate, proceed, reset, destroy

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

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.

Активация блокера

Каждый блокер должен быть активирован перед началом работы.

blocker.activate();

Использование в компоненте

При использовании блокера в компоненте, его необходимо активировать и деактивировать в useEffect:

useEffect(() => {
  blocker.activate();

  return () => {
    blocker.destroy();
  };
}, []);

Если этого не сделать, то в Strict Mode будут проблемы в работе блокера

Глобальный store

При использовании блокера вне компонента его можно активировать сразу в конструкторе класса:

class GlobalStore {
  constructor(private readonly _router: Router) {
    this.blocker = this._router.createBlocker((nextLocation) => {
      return nextLocation.pathname === '/main';
    });

    this.blocker.activate();
  }
}

Состояние блокировки

blocker.isBlocked является observable

const blocker = router.createBlocker((nextLocation) => {
    return nextLocation.pathname === '/main'
})

router.navigate('/main')

blocker.isBlocked // true

Управления блокировкой

// Активировать блокировку
blocker.activate()

// Продолжить переход на заблокированный маршрут.
blocker.proceed() 

// Отменить переход и остаться на текущей странице.
blocker.reset()

// Удалить блокер
blocker.destroy()

Работа с несколькими блокерами

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

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, блокер продолжит слушать изменения маршрутов, что может привести к непредсказуемому поведению в навигации.

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

blocker.activate();

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

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

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

API

Синтаксис

router.matchPath(pattern: MatchPathPattern | string, path: string): MatchPathInfo | null

Параметры

  • pattern - Паттерн маршрута (строка или объект MatchPathPattern)
    • При передаче строки используется точное сопоставление (isExact: true по умолчанию)
    • При передаче объекта MatchPathPattern можно указать isExact: false для частичного сопоставления
  • path - URL pathname для сопоставления

Возвращаемое значение

type MatchPathInfo = {
  params: Record<string, string>;    // Динамические параметры из URL
  pathname: string;                  // Полный совпадающий путь
  pathnameBase: string;              // Базовый путь для дочерних маршрутов
  pattern: string;                   // Исходный паттерн маршрута
};

Базовый пример

Самый простой способ использования - сопоставление строкового паттерна с URL:

// Сопоставление простого маршрута
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:

// Точное сопоставление (значение по умолчанию)
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

// Сопоставит любые пути, начинающиеся с '/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

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

pathname

Основное свойство pathname содержит текущий путь URL без query параметров и hash фрагмента.

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

// Получение текущего пути
const currentPath = router.location.pathname;
console.log(currentPath); // '/user/123'

Реактивное использование с MobX

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) — вызывается после успешного завершения перехода и рендера нового маршрута.

API

Синтаксис

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, которая отписывает обработчик от событий.

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

// Подписка на начало перехода:
const disposeStart = router.subscribeRouteChangeStart((location) => {
    console.log('Начинается переход на:', location.pathname);
});

// Отписка от события перехода:
disposeStart();

// Подписка на окончание перехода:
const disposeEnd = router.subscribeRouteChangeEnd((location) => {
    console.log('Переход завершён:', location.pathname);
});

// Отписка от события перехода:
disposeEnd();

Debug

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

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

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

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

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

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

Создание routerMock

shared/services/Router

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

shared/_test/RouterMock

import { createRouterMockFactory } from '@astral/mobx-router';

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

export const createRouterMock = createRouterMockFactory(router);

API

type Params = {
  initialLocation?: Location;
  initialSearchParams?: SearchParams;
};

Вспомогательные методы

waitNavigation

Ожидает завершения навигации.

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

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

Базовый пример

it('Удаляется организация, если активен home route', () => {
  const routerMock = createRouterMock({ initialLocation: { pathname: '/home' } });

  const sut = createStore(routerMock);

  expect(sut.orgId).toBeUndefined();
});

Работа с навигацией

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

it('Filters содержит начальные значения из url', async () => {
  const routerMock = createRouterMock({
    initialSearchParams: {
      page: 1,
      limit: 10,
    },
  });

  const sut = createStore(routerMock);

  expect(sut.filters).toEqual({
    page: 1,
    limit: 10,
  });
});
it('Фильтры удаляются из url после submit', async () => {
  const routerMock = createRouterMock({
    initialSearchParams: {
      page: 1,
      limit: 10,
    },
  });

  const sut = createStore(routerMock);

  await sut.submit();

  expect(routerMock.searchParams).toEqual({});
});

Работа с routeParams

...

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

...
it('orgId содержит значение из url', () => {
  const routerMock = createRouterMock({ initialLocation: { pathname: '/org/123' } });

  const sut = createStore(routerMock);

  expect(sut.orgId).toBe('123');
});