> Как предотвратить двойной заказ при двойном клике на кнопку? (JavaScript)

Уровень: senior · Роль: frontend · Категория: Технические вопросы

Компании: elirox

Стек: JavaScript

> Пример ответа

Короткий ответ

Основная стратегия - блокировка повторной отправки на время выполнения запроса. Используйте флаг isSubmitting или disable кнопки. Для надёжности добавьте debounce и идемпотентность на сервере. В современных React-приложениях удобно использовать useRef или useState с проверкой внутри обработчика. Также можно генерировать уникальный idempotency key для каждого заказа и передавать его на бэкенд.

Подробное объяснение

Проблема двойного заказа возникает из-за race condition: пользователь кликает дважды, и оба события успевают пройти до завершения первого запроса. Решения делятся на клиентские и серверные.

Клиентские подходы:

  • Флаг блокировки - самый простой и надёжный. Устанавливаем isSubmitting = true в начале обработчика и сбрасываем после ответа или ошибки. Кнопка при этом визуально disabled.
  • Debounce - отсекает повторные клики в течение короткого интервала (например, 300-500 мс). Полезен как дополнительная защита, но не заменяет флаг.
  • Отмена предыдущего запроса - через AbortController в fetch или takeLatest в Redux-Saga. Подходит, если повторный клик должен отменять предыдущий запрос, а не игнорироваться.

Серверные подходы (обязательны для критичных операций):

  • Идемпотентность - сервер должен обрабатывать одинаковые запросы как один. Для этого клиент генерирует уникальный ключ (UUID) и передаёт его в заголовке или теле. Сервер хранит ключи и при повторной отправке возвращает результат первого запроса.
  • Проверка состояния заказа - перед созданием проверяется, не существует ли уже заказ с таким же набором данных (например, по userId + productId + timestamp).

Важно понимать trade-off: клиентская защита не гарантирует 100% надёжность (пользователь может отправить запрос через DevTools или с другого устройства), поэтому для платёжных операций обязателен серверный контроль.

На практике

Для senior-позиции важно показать комплексный подход: не просто disable кнопки, а понимание всех уровней защиты. Начните с клиентского флага, затем добавьте debounce для UX, и обязательно упомяните идемпотентность на сервере.

Учитывайте edge cases:

  • Сброс флага в finally, чтобы не потерять состояние при ошибке.
  • Обработка повторного клика во время уже выполняющегося запроса - не просто игнорировать, а показать индикатор загрузки.
  • Для React 18+ с StrictMode двойной вызов эффектов в dev-режиме - проверьте, что ваш код устойчив к этому.

Пример кода

JAVASCRIPT
// React + hooks
function OrderButton({ productId }) {
const [isSubmitting, setIsSubmitting] = useState(false);
const requestIdRef = useRef(null);
const handleClick = async () => {
if (isSubmitting) return;
setIsSubmitting(true);
const idempotencyKey = crypto.randomUUID();
try {
const response = await fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify({ productId })
});
if (!response.ok) throw new Error('Order failed');
// обработка успешного ответа
} catch (error) {
// показать ошибку пользователю
} finally {
setIsSubmitting(false);
}
};
return (
<button
onClick={handleClick}
disabled={isSubmitting}
>
{isSubmitting ? 'Оформляем...' : 'Заказать'}
</button>
);
}

Как отвечать на собеседовании

Начните с простого решения (флаг), затем расширьте до полной картины. Покажите, что понимаете ограничения клиентской защиты. Обязательно упомяните идемпотентность - это ключевой момент для senior.

Структура ответа:

  1. Базовое решение: isSubmitting + disabled.
  2. Почему этого недостаточно: пользователь может обойти UI.
  3. Дополнительные меры: debounce, AbortController.
  4. Серверная идемпотентность - главная гарантия.
  5. Упомяните про finally для сброса состояния.

Если спросят про конкретные фреймворки - адаптируйте: для Vue можно использовать ref и v-loading, для Angular - Subject и take(1).

Что проверяет интервьюер

  • Понимание race conditions и асинхронности.
  • Умение мыслить на разных уровнях: UI, сеть, сервер.
  • Знание современных паттернов (idempotency key, AbortController).
  • Внимание к деталям: сброс флага в finally, обработка ошибок.
  • Способность объяснить trade-off между простотой и надёжностью.

Типичные ошибки

  • Только disable кнопки без проверки внутри обработчика - не защищает от программных кликов.
  • Забывают сбросить флаг при ошибке - кнопка остаётся заблокированной.
  • Не учитывают повторные клики до первого рендера (например, при быстром двойном клике до установки state).
  • Полагаются только на клиентскую защиту для финансовых операций.
  • Используют setTimeout для разблокировки вместо сброса по завершении запроса.
  • Не генерируют уникальный ключ для каждого заказа - повторная отправка того же ключа может привести к ошибке на сервере.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью