🫢

画面遷移前の“すき間時間”でボタンがクリックできてしまっていた話

に公開

こんにちは、ずっきーです!

フォーム送信時にボタンを非活性(disabled)状態にしても、画面遷移前の短い時間にボタンがクリックできてしまう問題がありました。原因と対策法について深掘りしていこうと思います。

問題の概要

ログインフォームで「次へ」ボタンを押すと API を呼び出し、
リクエスト成功後に別ページへリダイレクトする実装でした。

const onSubmit = async (values: FormValues) => {
  try {
    await fetch('/sample', {
      method: 'POST',
      body: JSON.stringify(values),
    });

    window.location.assign('/next');
  } catch (error) {
    // エラーハンドリング
  }
};

ボタンはReact Hook Formのform.formState.isSubmittingで非活性状態を管理していました。

<Button
  type="submit"
  disabled={form.formState.isSubmitting}>
  次へ
</Button>

一見すると送信中は操作できなさそうに見えますが、APIのレスポンスが返ってきてからwindow.location.assignで実際にページ遷移が完了するまでの間に、ボタンが再度クリックできてしまう問題が起きていました。

なぜ起きたのか

window.location.assign は同期的だが、ページ遷移は即座に完了しない

window.location.assign 自体は 同期的な関数なのですが、 実際のページ遷移が完了するまでにはラグがあることがポイントです!

window.location.assign('/next');
// ↑ この行を実行した瞬間に JS 的には「処理完了」になる
// でも、画面はまだ切り替わっていない

React Hook FormのisSubmittingの挙動

React Hook FormのisSubmittingは、handleSubmitに渡した関数(onSubmit)が完了するまでtrueです。

const onSubmit = async (values: FormValues) => {
  // 1. この時点で isSubmitting = true
  
  await fetch('/sample', {...});
  // 2. APIレスポンスが返ってきた時点で isSubmitting = true(まだ完了していない)
  
  window.location.assign('/next');
  // 3. window.location.assign を呼んだ時点で isSubmitting = true
  // 4. しかし、この onSubmit はここで完了してしまうため、isSubmitting = false になる
  // 5. 実際のページ遷移が完了するまでの間に、ボタンが再度クリック可能になってしまう
};

window.location.assignは同期的に実行され、関数は即座に完了します。そのため、onSubmitが完了してisSubmittingfalseになり、実際のページ遷移が完了するまでの間にボタンがクリック可能になっていました。

解決策の検討

アプローチ1: stateで管理する方法

独自のstate(isNavigatingなど)を追加して、ページ遷移中かどうかを管理する方法です。

const [isNavigating, setIsNavigating] = useState(false);

const onSubmit = async (values: FormValues) => {
  try {
    await fetch('/sample', {
      method: 'POST',
      body: JSON.stringify(values),
    }).then((res) => res.json());
    
    setIsNavigating(true);
    window.location.assign('/next');
  } catch (error) {
    // エラーハンドリング
  }
};

// ボタン側
<Button
  type="submit"
  disabled={form.formState.isSubmitting || isNavigating}
>
  次へ
</Button>

メリット:

  • Reactでよく使われるパターンなので、誰が見ても理解しやすい

デメリット:

  • React Hook FormのisSubmittingと別のstateを管理する必要がある

アプローチ2: Promiseをresolveしない方法

window.location.assignを呼んだ後、Promiseをresolveしないことで、onSubmitが完了しないようにする方法です。

const onSubmit = async (values: FormValues) => {
  try {
    await fetch('/sample', {
      method: 'POST',
      body: JSON.stringify(values),
    }).then((res) => res.json());
    
    window.location.assign('/next');
    // window.location.assign を呼んで実際にページ遷移が起こるまでの間にラグがあるため、Promiseをresolveしない
    await new Promise(() => {});
  } catch (error) {
    // エラーハンドリング
  }
};

メリット:

  • 追加のstate管理が不要で、コードがシンプル
  • React Hook FormのisSubmittingをそのまま活用できる

デメリット:

  • ぱっと見だと「Promiseをresolveしない」という実装が意図を理解しづらい可能性がある(コメントを残せば大丈夫そう)

採用した解決策

今回は、コードのシンプルさを重視してPromiseをresolveしない方法を採用しました ⭐

最後に

最後まで読んでくださりありがとうございます!

同じような問題で詰まっている方がいたら参考になれば嬉しいです。

Social PLUS Tech Blog

Discussion