はじめに
API処理を伴うボタンで連続クリックの制御を行っているはずなのに、なぜかAPIが複数回呼ばれてしまうことはありませんか?
それは、処理完了後にボタンが再び有効になってしまうことが原因かもしれません。
最近、画面遷移を伴う処理の際に遷移するまでにボタンを複数回押せてしまう場面に遭遇したので、その時の解決方法をまとめてみました。
同じ悩みを抱える方の参考になれば幸いです。
解決策:ページ遷移まで制御を維持する設計
今回のケースで重要なのは、成功時は状態をリセットせずにページ遷移で自然に解決することです。
const [isLoading, setIsLoading] = useState(false)
const [isDisabled, setIsDisabled] = useState(false)
const handleSubmit = async () => {
setIsLoading(true)
setIsDisabled(true)
try {
await apiCall()
setIsLoading(false) // ローディングのみ解除
// isDisabledは意図的にリセットせず、ページ遷移で自動的に解決する
router.push('/success')
} catch (error) {
console.error('Submit error:', error)
// エラー時は両方をリセットして再実行可能にする
setIsLoading(false)
setIsDisabled(false)
}
}
設計のポイント
1. loading と disabled の分離
isLoadingはスピナー表示用、isDisabledはボタン無効化用として役割を分離
2. 成功時は部分的にリセット
API処理完了後はisLoadingのみ解除し、isDisabledは維持してページ遷移まで操作を無効化
3. エラー時は完全にリセット
エラーが発生した場合は両方の状態をリセットし、再度押下できるように
4. UI で適切な制御
<LoadingButton
onClick={handleSubmit}
disabled={isDisabled || !isFormValid} // 複数条件の組み合わせ
loading={isLoading}
>
送信する
</LoadingButton>
このように分離することで、API処理完了後もボタンを無効化にしたまま、スピナーのみ停止する(ボタンの処理が終わったことを伝える)ことができます。
まとめ
この実装により、API 処理完了からページ遷移までの「隙間時間」での重複実行を防ぐことが可能です。
ポイントは成功時に状態をリセットせずにエラー時のみリセットする設計、フロント処理完了 ≠ ボタン有効化という考え方です。