はじめに
Qiitaキャンペーン「アクセシビリティの知見を発信しよう!」にのっかって、意外と見落としがちなユーザー操作である、ボタンの連打対応(連打による多重リクエスト発生の解消対応)について記事を書いてみようと思います。
ボタン連打によるリクエスト大量発生を防ぐことができるのは勿論ですが、Aの処理中にBの処理が走ってしまうことも防ぐことができて一石二鳥です。
※GPT-4oが生成した「一石二鳥」のイメージ図
※まるで2羽のTwitterが1羽のXに変化したようですね(?)
前提
そもそもどんなシステムなのか
フロントがVue.js(Nuxt.js)、バックエンドがNode.jsを載せたLambdaという構成のシステムを開発しています。TypeScriptを使用している、一般的なTSアプリケーションです。
ボタンを押下した際に指定したデータがDBに登録されるような場面で、ボタンを連打すると不用意にリクエストが複数走ってしまいます。また、いくらトランザクションを張ってるとはいえ、登録処理中に他画面に遷移するような操作は未然に防いでおきたいです。
対策
まず実装したこと
最初の押下タイミングで変数Aをtrue
にセットして、その状況下では押下しても処理が走らないようにしました。
サーバーからレスポンスが返ってきて、レスポンスごとの処理を完了させた時点で変数Aをfalse
にセットして、次の押下リクエストを許可します。
/** 連打防止フラグ */
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
を使用します。(参考サイト)
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
を設置しています。
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
は以下の様に呼び出されます。
/** 押下時の処理 */
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
に連打防止対応を仕込む内容を執筆できればと思います。ここまで読んでいただき、ありがとうございます。