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

В React Router предусмотрены механизмы для контроля навигации между маршрутами, включая возможность блокировки переходов при определённых условиях. Это особенно важно в случаях, когда нужно предупредить пользователя о потере несохранённых данных или остановить переход в зависимости от состояния приложения.

usePrompt и Prompt в React Router

В версии React Router до v6 использовался компонент <Prompt> для блокировки навигации. Он принимал два основных свойства:

  • when — логическое значение, указывающее, активна ли блокировка.
  • message — сообщение или функция, возвращающая сообщение для подтверждения перехода.

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

import { Prompt } from 'react-router-dom';
import { useState } from 'react';

function Form() {
  const [isBlocking, setIsBlocking] = useState(false);

  return (
    <form
      onCha nge={() => setIsBlocking(true)}
      onSub mit={() => setIsBlocking(false)}
    >
      <Prompt
        when={isBlocking}
        message="Есть несохранённые изменения. Вы уверены, что хотите покинуть страницу?"
      />
      <input type="text" placeholder="Введите данные" />
      <button type="submit">Сохранить</button>
    </form>
  );
}

В этом примере навигация будет заблокирована, пока поле формы содержит изменения. Пользователь получит предупреждение при попытке уйти со страницы.

Блокировка навигации в React Router v6

В React Router v6 компонент <Prompt> был удалён. Для аналогичной функциональности используется хук useBlocker, который можно реализовать через useNavigate и useLocation.

Создадим кастомный хук usePrompt на базе нового API:

import { useContext, useEffect } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';

function useBlocker(blocker, when = true) {
  const navigator = useContext(NavigationContext).navigator;

  useEffect(() => {
    if (!when) return;

    const unblock = navigator.block((tx) => {
      const autoUnblockingTx = {
        ...tx,
        retry() {
          unblock();
          tx.retry();
        }
      };
      blocker(autoUnblockingTx);
    });

    return unblock;
  }, [navigator, blocker, when]);
}

export function usePrompt(message, when = true) {
  const blocker = (tx) => {
    if (window.confirm(message)) {
      tx.retry();
    }
  };

  useBlocker(blocker, when);
}

Применение хука в компоненте формы:

import { useState } from 'react';
import { usePrompt } from './usePrompt';

function Form() {
  const [isBlocking, setIsBlocking] = useState(false);

  usePrompt("Есть несохранённые изменения. Вы уверены, что хотите покинуть страницу?", isBlocking);

  return (
    <form
      onCha nge={() => setIsBlocking(true)}
      onSub mit={() => setIsBlocking(false)}
    >
      <input type="text" placeholder="Введите данные" />
      <button type="submit">Сохранить</button>
    </form>
  );
}

Контроль перехода по кнопкам и ссылкам

Блокировка навигации работает не только при использовании истории браузера, но и при переходе через <Link> или navigate(). Внутри blocker можно вызывать tx.retry() для подтверждённого перехода или отменять переход.

const blocker = (tx) => {
  if (window.confirm("Вы хотите покинуть страницу с несохранёнными изменениями?")) {
    tx.retry(); // продолжает навигацию
  }
  // иначе переход отменяется автоматически
};

Использование состояния для динамической блокировки

Часто требуется блокировать переходы только при определённых условиях, например, если форма содержит изменения:

const [isFormDirty, setIsFormDirty] = useState(false);

usePrompt(
  "Вы уверены, что хотите уйти, не сохранив изменения?",
  isFormDirty
);

Блокировка автоматически активируется при изменении состояния isFormDirty и снимается, если состояние меняется на false.

Ограничения и особенности

  • usePrompt и useBlocker в React Router v6 не являются официальным публичным API и используют UNSAFE_NavigationContext. Это значит, что при обновлении версии библиотеки поведение может измениться.
  • Блокировка работает только для навигации внутри приложения через React Router. Для полной защиты при закрытии вкладки необходимо использовать событие beforeunload:
useEffect(() => {
  const handleBeforeUnload = (e) => {
    if (isFormDirty) {
      e.preventDefault();
      e.returnValue = '';
    }
  };

  window.addEventListener('beforeunload', handleBeforeUnload);
  return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [isFormDirty]);
  • Необходимо учитывать, что некоторые браузеры игнорируют кастомные сообщения в beforeunload и показывают стандартный текст предупреждения.

Практические советы

  • Всегда связывать блокировку с состоянием, чтобы избежать ненужных предупреждений.
  • Использовать tx.retry() для повторного выполнения навигации после подтверждения.
  • Для сложных форм и многостраничных приложений комбинировать блокировку навигации с локальным состоянием сохранения данных.

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