Почему форма отправляется два раза при сабмите в React
Что проверяют
Умеете ли вы разбирать живой баг, а не пересказывать документацию. Причин у двойной отправки несколько, и хороший ответ это короткий список гипотез с проверкой каждой, плюс понимание, что клиентской защиты мало.
Как отвечать
Перебрать источники по порядку:
- не вызван
e.preventDefault(). Браузер делает свою отправку формы поверх вашей, и запрос уходит дважды: нативно и черезfetch; - кнопка внутри формы по умолчанию имеет
type="submit". Обработчик наonClickкнопки плюсonSubmitформы дают два вызова на один клик; - обработчик висит и на форме, и на родителе, либо подписка навешена в
useEffectбез снятия: после повторного монтирования слушателей становится два; - в
StrictModeв режиме разработки React монтирует компонент дважды и повторно прогоняет эффекты. Если запрос уходит из эффекта, в разработке он уйдёт дважды, а в сборке один раз; - пользователь успевает кликнуть второй раз, пока идёт запрос.
Рабочая защита выглядит так:
const sending = useRef(false);
async function onSubmit(e) {
e.preventDefault();
if (sending.current) return;
sending.current = true;
try { await send(new FormData(e.currentTarget)); }
finally { sending.current = false; }
}Плюс disabled на кнопке для видимой обратной связи и ключ идемпотентности в запросе, чтобы повтор не создал вторую сущность.
Где спотыкаются
Защищаются только состоянием. setState применяется асинхронно, поэтому два клика в одном тике оба увидят старое значение флага. Нужен ref или атрибут disabled, который снимает второй клик на уровне браузера.
Списывают всё на StrictMode и выключают его. Двойной вызов в разработке это диагностика: он показывает, что эффект не идемпотентен. Отключение прячет проблему ровно до боевой нагрузки.
Забывают про сервер. Повтор придёт после обрыва сети, ретрая или обновления страницы, и никакая кнопка этого не остановит. Защита от дубля должна быть на приёмной стороне.