はるさめ.dev

無効なボタンに aria-disabled を使ったときの React Hook Form で二重送信防止

投稿日:Sun Dec 31 2023 09:24:09 GMT+0000 (Coordinated Universal Time)

背景

form で submit ボタンといえば当たり前のように disabled を使用して二重送信を防いだり、バリデーションにエラーが発生しているときに送信できないようにしていました。
しかし、disabled を使うと Tab キーでフォーカスが当たらないため支援技術を使用しているユーザーがボタンの存在を認識できないという問題があるようです。(参考リンク参照のこと)

React Hook Form で二重送信を防ぐために form.formState.isSubmitting を disabled に指定していたところを aria-disabled に少しだけつまづいたので忘録として残します。

解決策

少しスマートではないですが、formState.isSubmitting が true の場合は onSubmit に重複 submit 防止用の関数を割り当てて、 false の場合は handleSubmit を割り当てました。

const [message, setMessage] = useState("");
const form = useForm({});

// submit の処理
const handleSubmit = form.handleSubmit(async (data)=>{
  await api(data);
})

// submit の中断とエラーメッセージ設定
const preventDuplicatedSubmit = (e)=>{
  e.preventDefault();
  message = setMessage("すでに送信中のため送信処理を実行しませんでした。")
}

return (
  <form onSubmit={form.formState.isSubmitting ? preventDuplicatedSubmit : handleSubmit}> // 
    {/* input fields ... */}
    <button aria-disabled={form.formState.isSubmitting}>Submit</button>
    <div aria-live="assertive"> // 重複実行時はエラーのメッセージを表示。
      {message}
    </div>
  </form>
)

つまづいた点

最初は handleSubmit 内のイベントハンドラで form.formState.isSubmitting が true のときは早期 return するように実装しました。
しかし当たり前でしたが、 form.handleSubmit() 内で form.formState.isSubmitting は常に true なのでメインの処理が実行できません。

const handleSubmit = form.handleSubmit(async (data)=>{
  // 常に true のため毎回早期 return される
  if (form.formState.isSubmitting){
    return;
  }
  
  await api(data); // ここに到達しない
})

続いて isProcessing のような変数を form.handleSubmit 内で true / false 切り替えて、 true の場合は早期 return するように変更しました。
すると早期 return したタイミングで form.handleSubmit.isSubmitting が false になってしまいました。

const [isProcessing, setIsProcessing] = useState(false);

const handleSubmit = form.handleSubmit(async (data)=>{
  if (isProcessing) {
    return; // ここで return が実行されると form.formState.isSubmitting が false になってしまう
  }

  setIsProcessing(true);
  
  await api(data);

  setIsProcessing(false);  // ホントはここまで実行されたあとに form.formState.isSubmitting が false になって欲しい
})


return (
  <form onSubmit={form.formState.isSubmitting ? undefined : handleSubmit}>
    {/* input fields */}
    <button aria-disabled={form.formState.isSubmitting}>Submit</button> {/* aria-disabled だと opacity が設定されて薄くなるようにしているが、2回目クリックすると opacity が外れてしまう */}
  </form>
)

ということで解決策に記載の方法に落ち着きました。

参考

コメント