◾️ はじめに
自社サービスでは、注文状況のステータスを変更すると、ユーザーに通知が送信される仕組みを導入しています。
しかし、通信環境が不安定な場合や、ステータス変更処理に時間がかかる場合に、ユーザーが変更ボタンを連打してしまい、同じ通知が複数回送信されるという問題が発生しました。
◾️ 初期対応
クリックの連打を防止するため、以下の2つの属性をボタンに追加しました。
:loading="loading"
:disabled="loading"
これにより、ボタンをクリックした直後にloadingがtrueとなり、ローディング状態が表示されると同時にボタンが非活性化されます。
そのため、「処理中の再クリックを防止できる」と考えました。
しかし、実際には、この対応だけでは不十分でした。
loading = trueが反映される直前に素早く2回クリックされると、非活性になる前に2回分のクリックイベントが通ってしまい、submitRequestが複数回実行されてしまうという問題が残りました。
※全体のコードです。
<script setup lang="ts">
const loading = ref(false)
const submitRequest = async () => {
loading.value = true
try {
const orderId = orderInfo.value.id
await $fetch(
// API叩く処理省略
)
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
}
</script>
<template>
<div class="d-flex flex-column ga-2">
<v-btn
color="danger-red"
class="text-subtitle-1"
:loading="loading"
:disabled="loading"
@click="submitRequest"
>変更する</v-btn
>
</div>
</template>
◾️ 原因
この現象は、loading.value = true が非同期関数内で実行されるため、JavaScriptのイベントループによりDOMへの反映にわずかな遅延が生じることに起因します。
つまり、1回目のクリックと2回目のクリックの間に、loadingがtrueに切り替わる前のごく短いタイミングが存在し、その間にもう一度クリックされると、2回目の実行が通ってしまうのです。
◾️ 解決策:once で関数を一度だけ実行
この問題への対応として、lodashのonce関数を用いて、「関数が一度だけしか実行されない」よう制御できるのではないかと考えました。
onceを使うことで、ダブルクリックだけでなく、その後の再クリックも完全に防ぐことができます。
⸻
✅ 実装例(lodash の once を使用)
yarn add lodash
# or
npm install lodash
<script setup lang="ts">
import { once } from 'lodash'
const loading = ref(false)
const _submitRequest = async () => {
loading.value = true
try {
const orderId = orderInfo.value.id
await $fetch(
// API叩く処理省略
)
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
}
// once で1回だけ実行される関数を作成
const submitRequest = once(_submitRequest)
</script>
<template>
<div class="d-flex flex-column ga-2">
<v-btn
color="danger-red"
class="text-subtitle-1"
:loading="loading"
:disabled="loading"
@click="submitRequest"
>変更する</v-btn
>
</div>
</template>
この方法は「完全に1回のみの実行」を保証できる反面、通信に失敗しても再実行ができないという制約があります。
ユースケースに応じて、選定には注意が必要です。
◾️ さいごに
クリックの連打によってAPIが複数回呼ばれる問題は、UXだけでなくサービスの信頼性にも影響します。
ユースケースによってはloadingなどの状態管理と併用して柔軟に対応することが重要だと思います。