2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

アクセシビリティの知見を発信しよう!

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

Last updated at Posted at 2024-05-20

はじめに

Qiitaキャンペーン「アクセシビリティの知見を発信しよう!」にのっかって、意外と見落としがちなユーザー操作である、ボタンの連打対応(連打による多重リクエスト発生の解消対応)について記事を書いてみようと思います。

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

※GPT-4oが生成した「一石二鳥」のイメージ図
※まるで2羽のTwitterが1羽のXに変化したようですね(?)
image.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を使用します。(参考サイト

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ではdoubleHitControlFlgstateを用いて管理しており、更新用のメソッドとして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の引数を工夫することでローディング中の表示などもユニークに実装することができるかと思います。(参考サイト

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

2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?