【Vue.js】クリックの暴走を止める！連打防止戦略

# はじめに
Qiitaキャンペーン「[アクセシビリティの知見を発信しよう！](https://qiita.com/official-events/4d9893df2bfd52211392)」にのっかって、意外と見落としがちなユーザー操作である、ボタンの連打対応（連打による多重リクエスト発生の解消対応）について記事を書いてみようと思います。

ボタン連打によるリクエスト大量発生を防ぐことができるのは勿論ですが、Aの処理中にBの処理が走ってしまうことも防ぐことができて一石二鳥です。

※GPT-4oが生成した「一石二鳥」のイメージ図
※まるで2羽のTwitterが1羽のXに変化したようですね（？）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1953297/39e1ec94-f80d-a05b-bec2-d86e97173e5a.png)

# 前提

### そもそもどんなシステムなのか
フロントがVue.js（Nuxt.js）、バックエンドがNode.jsを載せたLambdaという構成のシステムを開発しています。TypeScriptを使用している、一般的なTSアプリケーションです。

ボタンを押下した際に指定したデータがDBに登録されるような場面で、ボタンを連打すると不用意にリクエストが複数走ってしまいます。また、いくらトランザクションを張ってるとはいえ、登録処理中に他画面に遷移するような操作は未然に防いでおきたいです。

# 対策
### まず実装したこと
最初の押下タイミングで変数Aを`true`にセットして、その状況下では押下しても処理が走らないようにしました。

サーバーからレスポンスが返ってきて、レスポンスごとの処理を完了させた時点で変数Aを`false`にセットして、次の押下リクエストを許可します。

```index.ts
/** 連打防止フラグ */
const doubleHitControlFlg = ref(false);

/** 押下時の処理 */
const clickMethod = async () => {
    if (doubleHitControlFlg.value) return;
    doubleHitControlFlg.value = true;
    await useFetch("/test/click-single-request", {
        method: "GET",
    })
    .then((res) => {
        console.log(`Res:${res}`);
        doubleHitControlFlg.value = false;
    })
    .catch((error) => {
        console.log(`Error:${error}`);
        doubleHitControlFlg.value = false;
    });
};
```

上記の処理でも十分当初の狙いは達成できていたのですが、`useFetch`使用箇所すべてにこのプログラムを設置していくことを考えると、少し記述量が多いです。運用していくうちに未対応の箇所が生まれてしまうかもしれません。（保守性が低い）

### 保守性を高くしてみる
「どこの処理でも、これを呼び出しさえすればOK」みたいな防止策のほうがいいですよね。なので、`utils`配下にメソッド群を管理する適当なファイルを用意して、そこに呼び出し元の処理を設置してみます。

まず`store`配下に、先ほど使用した変数Aを管理する適当なファイルを用意します。
状態管理には`Vuex`ではなく`Pinia`を使用します。（[参考サイト](https://qiita.com/tatsuki-tsuchiyama/items/b4ef100777954ea58821)）

```store/index.ts
import { defineStore } from "pinia";
export const useStore = defineStore("main", {
    state: () => ({
        /** 連打防止フラグ */
        doubleHitControlFlg: false,
    }),
    actions: {
        /** 連打防止フラグ更新メソッド */
        setDoubleHitControlFlg(flag: boolean) {
            this.doubleHitControlFlg = flag;
        },
        /** 連打防止フラグ取得メソッド */
        getDoubleHitControlFlg() {
            return this.doubleHitControlFlg;
        },
    },
});
```

`store`では`doubleHitControlFlg`を`state`を用いて管理しており、更新用のメソッドとして`setDoubleHitControlFlg`を、取得用のメソッドとして`getDoubleHitControlFlg`を設置しています。

```utils/index.ts
const {
    setDoubleHitControlFlg,
    getDoubleHitControlFlg,
} = useStore();

/** 連打防止処理 */
export const doubleClickGuard = async (execFun: Function, errorFun: Function) => {
  if(getDoubleHitControlFlg()) return;
  setDoubleHitControlFlg(true);

  try {
    await execFun();
  } catch (error) {
    errorFun(error);
  } finally {
    setDoubleHitControlFlg(false);
  }
};
```

`util`では`getDoubleHitControlFlg`を呼び出して連打防止フラグを取得しています。連打防止フラグが`true`の場合は、これ以上処理が続かないように`return`しています。

最初のブロックを通過すると`setDoubleHitControlFlg`により連打防止フラグを`true`に設定しています。これにより、`finally`で連打防止フラグが`false`に設定されるまでは最初のブロックで処理が`return`されるようになります。

この`doubleClickGuard`は以下の様に呼び出されます。
```index.ts
/** 押下時の処理 */
const clickMethod = async () => {
    await doubleClickGuard(
        async () => {
            await useFetch("/test/click-single-request", {
                method: "GET",
            })
            .then((res) => {
                console.log(`UseFetch Res:${res}`);
            })
            .catch((error) => {
                console.log(`UseFetch Error:${error}`);
            });
        },
        (error: Object) => {
            console.log(`DoubleClickGuard Error:${error}`);
        }
    );
};
```

thenやcatchの中に何を実装するのかは各PJ次第ではありますが、`console.log`の中身を確認すると、どのブロックがどの処理を受け取るブロックなのか理解できるかと思います。

# おわりに
今回は連打を防止することのみにフォーカスしていましたが、`doubleClickGuard`の引数を工夫することでローディング中の表示などもユニークに実装することができるかと思います。（[参考サイト](https://pg-log.com/nuxt-loading/)）

より保守性を高めることを目指して、次回は`middleware`に連打防止対応を仕込む内容を執筆できればと思います。ここまで読んでいただき、ありがとうございます。