0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

保存ボタンを連打されたらどうする?二重登録を防ぐUIとAPIの考え方(React/TypeScript)

0
Last updated at Posted at 2026-07-10

この記事の対象読者

  • フォーム送信や作成画面を実装する人
  • 保存ボタンの連打対策をどう考えればよいか迷っている人
  • フロントエンドとAPIの両方で二重登録を防ぎたい人

本題に入る前に

管理画面で保存ボタンを押したあと、画面の反応が少し遅いと、もう一度押したくなることがありますよね。

使う側からすると自然な操作です。「本当に押せたかな?」と不安になって、もう一度クリックする。これはユーザーが悪いというより、画面の反応が分かりにくい状態です。

ただ、作る側から見ると、保存処理が2回走ったり、同じデータが2件登録されたりすることがあります。問い合わせ対応で「同じものが2つできています」と言われると、地味に焦ります。

この記事では、Reactの送信中状態と、API側の二重登録対策を分けて考えます。

UIだけでは最後の防波堤にならない

まず、フロントエンドで保存中はボタンを押せないようにするのは大事です。

ただし、UIの disabled だけで完全に守れるわけではありません。通信の再送、ブラウザの戻る操作、別タブ、APIの直接呼び出しなど、フロントを通らない経路もあります。

画面では「押しにくくする」、APIでは「二重処理されないようにする」と責任を分けると考えやすいです。

Reactで送信中状態を持つ

最小例です。

import { useState } from "react";

export function ReservationForm() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [message, setMessage] = useState("");

  async function handleSubmit() {
    if (isSubmitting) return;

    setIsSubmitting(true);
    setMessage("");

    try {
      await fetch("/api/reservations", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          title: "サンプル予約",
          startsAt: "2026-07-01T10:00:00+09:00",
        }),
      });

      setMessage("保存しました");
    } catch {
      setMessage("保存に失敗しました。時間をおいて再度お試しください。");
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    <div>
      <button type="button" disabled={isSubmitting} onClick={handleSubmit}>
        {isSubmitting ? "保存中..." : "保存"}
      </button>
      {message && <p>{message}</p>}
    </div>
  );
}

ポイントは2つです。

  • isSubmittingtrue の間は再送しない
  • ボタン文言を変えて、押したことが分かるようにする

これだけでも、ユーザーの不安はかなり減ります。

API側では冪等キーを考える

フロントで止めても、API側の対策は別で必要です。

たとえば、作成リクエストに idempotencyKey を含めます。

type CreateReservationRequest = {
  title: string;
  startsAt: string;
  idempotencyKey: string;
};

API側では、同じ idempotencyKey の処理がすでに成功していないかを確認します。

async function createReservation(input: CreateReservationRequest) {
  const existing = await db.idempotencyKey.findUnique({
    where: { key: input.idempotencyKey },
  });

  if (existing) {
    return { status: "already_processed" };
  }

  const reservation = await db.reservation.create({
    data: {
      title: input.title,
      startsAt: input.startsAt,
    },
  });

  await db.idempotencyKey.create({
    data: {
      key: input.idempotencyKey,
      resourceType: "reservation",
      resourceId: reservation.id,
    },
  });

  return { status: "created", reservation };
}

実際には、DBトランザクションや一意制約も一緒に考えます。

どこまでやるかは処理の重さで決める

すべてのボタンに大げさな仕組みが必要なわけではありません。

処理 対策の目安
検索条件の反映 UIで連打を抑えるくらいでよいことが多い
予約や注文の作成 API側の重複対策も考える
メール送信や請求 冪等キーや一意制約をかなり真面目に考える

「2回走ったら困るか?」を基準にすると判断しやすいです。

まとめ

保存ボタンの連打対策は、フロントだけでもAPIだけでも不十分になりがちです。

画面では送信中状態を出して、ユーザーに「押せている」と伝える。APIでは、同じ処理が2回来ても二重登録されないようにする。

この2つを分けて考えると、フォーム送信はかなり落ち着いて設計できます。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?