Блокировка навигации

Блокировка навигации — это механизм, позволяющий контролировать переходы между маршрутами, предотвращать потерю несохранённых данных и реализовывать условную маршрутизацию. В TanStack Router этот процесс гибко настраивается через хуки жизненного цикла маршрутов и функции-guards. Рассмотрим их подробности.


Использование beforeUnload и block для глобальной блокировки

TanStack Router позволяет перехватывать попытки навигации с помощью функции block. Этот метод возвращает колбэк, который вызывается каждый раз, когда пользователь или приложение пытается перейти на другой маршрут. Основной синтаксис:

import { router } from './router';

const unblock = router.block((tx) => {
  if (!confirm('У вас есть несохранённые изменения. Выйти?')) {
    return false; // блокировка перехода
  }
  tx.retry(); // разрешение перехода
});
  • tx — объект транзакции перехода, содержащий метод retry(), который позволяет повторить переход после принятия решения.
  • Возврат false из колбэка полностью блокирует навигацию.
  • Для отмены блокировки используется вызов unblock().

Такой подход полезен для реализации предупреждений о несохранённых данных на уровне всего приложения.


Локальные guards на уровне маршрута

Для контроля переходов внутри конкретного маршрута TanStack Router предоставляет опцию beforeExit. Она подключается к объекту маршрута и вызывается перед уходом со страницы:

const profileRoute = {
  path: '/profile',
  component: ProfilePage,
  beforeExit: ({ router, next }) => {
    if (unsavedChanges()) {
      if (!confirm('Сохранить изменения перед выходом?')) {
        return false; // блокировка перехода
      }
    }
    next(); // разрешение перехода
  }
};
  • next() — продолжение навигации после проверки.
  • Guard может быть асинхронным, что позволяет выполнять промисы, например, сохранять данные на сервере перед переходом.

Асинхронная блокировка и промисы

TanStack Router поддерживает асинхронные guards, что удобно для сложной логики. Например:

const settingsRoute = {
  path: '/settings',
  component: SettingsPage,
  beforeExit: async ({ next }) => {
    const shouldExit = await showSaveDialog();
    if (!shouldExit) return false; // блокировка перехода
    next();
  }
};
  • Функция beforeExit может возвращать Promise или void.
  • Асинхронная проверка позволяет интегрировать сторонние библиотеки диалогов, API-сохранение и другие действия до завершения навигации.

Комбинация beforeExit и глобального block

Для сложных приложений часто используют оба метода одновременно:

  1. Глобальная блокировка защищает от навигации вне компонентов, например, клики на ссылках или действия браузера.
  2. Маршрутные guards обеспечивают локальные проверки и бизнес-логику для конкретного маршрута.
router.block((tx) => {
  if (unsavedGlobalChanges()) {
    if (!confirm('Вы уверены, что хотите покинуть сайт?')) {
      return false;
    }
  }
  tx.retry();
});

Сценарии применения блокировки навигации

  1. Несохранённые формы — предотвращение потери данных при переходе между страницами.
  2. Редиректы на авторизацию — блокировка доступа к маршруту до выполнения проверок.
  3. Асинхронные проверки — сохранение данных на сервере перед переходом на другой маршрут.
  4. Модальные окна с подтверждением выхода — интеграция блокировки навигации с интерфейсными компонентами.

Важные моменты и рекомендации

  • Не блокировать навигацию без необходимости: чрезмерное использование block ухудшает UX.
  • Асинхронные guards должны всегда вызывать next() или возвращать false для предсказуемого поведения.
  • Отмена блокировки через вызов unblock() обязательна при размонтировании компонентов, чтобы предотвратить утечки памяти.
  • Для SPA рекомендуется использовать комбинацию локальных и глобальных guards для гибкого управления переходами.

Пример комплексной реализации

import { createRouter } from '@tanstack/router';

const router = createRouter({
  routes: [
    {
      path: '/edit',
      component: EditPage,
      beforeExit: async ({ next }) => {
        if (hasUnsavedChanges()) {
          const confirmExit = await showExitConfirmation();
          if (!confirmExit) return false;
        }
        next();
      }
    },
    { path: '/', component: HomePage }
  ]
});

// Глобальная блокировка для всех маршрутов
const unblock = router.block((tx) => {
  if (globalUnsavedChanges()) {
    if (!confirm('Сохранить изменения перед уходом?')) return false;
  }
  tx.retry();
});

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