Как предотвратить двойной заказ при двойном клике на кнопку?
Что проверяют
Понимаете ли вы, что дубль заказа это проблема не только кнопки. Интерфейс закрывает случайный двойной клик, а гарантию даёт только сервер. Следом спрашивают: а если пользователь откроет две вкладки или запрос повторится после таймаута?
Как отвечать
Защиту стройте в два слоя.
На клиенте:
- блокировка на время запроса: кнопка получает
disabledи индикатор загрузки, пока запрос не завершился. После ошибки кнопка снова активна, после успеха пользователя уводят на страницу заказа; - синхронный флаг: в React проверка
isSubmittingиз состояния читает значение из замыкания текущего рендера, поэтому надёжнее флаг вuseRef, который выставляется до вызоваfetch. В TanStack Query похожую роль играетisPendingуuseMutation; - отправка через форму: обработчик на
onSubmit, а не наonClick, тогда Enter в поле ввода проходит тот же путь с той же блокировкой.
На сервере нужен ключ идемпотентности. Клиент генерирует crypto.randomUUID() один раз при открытии оформления и шлёт его в заголовке Idempotency-Key. Сервер хранит ключ вместе с результатом, и повтор с тем же ключом получает уже созданный заказ, а не новый. Ключ живёт в состоянии формы и не меняется между повторами, иначе смысла в нём нет.
Такой ответ показывает, что вы видите сценарии, до которых клиент не дотягивается: две вкладки, автоматический повтор запроса, повторную отправку формы после возврата назад.
Где спотыкаются
Предлагают debounce. Он задерживает первый клик и спасает только от кликов в пределах задержки: второй клик через секунду, пока запрос ещё в пути, спокойно уйдёт вторым заказом.
Генерируют ключ идемпотентности внутри обработчика клика. Тогда у каждого клика свой ключ, и сервер честно создаст два заказа.
Блокируют кнопку и забывают разблокировать её при ошибке сети. Пользователь остаётся перед мёртвой кнопкой и перезагружает страницу, теряя введённые данные.