1. ホーム
  2. React

【React】useFormStatus の使い方|フォーム送信中の状態を子コンポーネントから取得する

Share

フォームを送信している間、送信ボタンを無効化して「送信中…」と表示したい、という場面はよくあります。React には、こうした「フォームの送信中かどうか」をボタン側のコンポーネントから受け取れる useFormStatus というフックが用意されています。ポイントは、この状態を親の <form> から props で渡さなくても、フォームの内側にあるコンポーネントが自分で取得できることです。この記事では、useFormStatus の基本的な使い方から、送信ボタンを別コンポーネントに切り出す理由、戻り値のプロパティ、そして useActionState との使い分けや、うまく動かないときの原因までを順番に解説します。

useFormStatus は何をするフックか

useFormStatusreact-dom が提供するフックで、親にある <form> の送信状態を、そのフォームの内側にあるコンポーネントから読み取るためのものです。インポートは react ではなく react-dom から行う点に注意してください。

import
import { useFormStatus } from 'react-dom';

このフックが便利なのは、送信中かどうかという状態を親から子へ props で受け渡す必要がない点です。フォームの中に置かれた送信ボタンのコンポーネントが、自分自身で「今このフォームは送信中か」を知ることができます。そのため、深くネストしたボタンでも、途中のコンポーネントを経由して状態をバケツリレーする必要がありません。

送信ボタンを別コンポーネントに切り出す必要がある

useFormStatus を使ううえで最初に理解しておきたいのが、このフックは「その <form> の子孫コンポーネント」の中で呼ばなければならない、という制約です。ここを外すと状態が取れず、多くの人が最初につまずくポイントになります。

useFormStatus は、自分より上位にある一番近い <form> の状態を参照します。裏を返すと、<form> を書いているコンポーネントと同じ場所で useFormStatus を呼んでも、そのフォームは「上位」には存在しないため状態を取得できません。この場合 pending は常に false のままになります。

そこで定石となるのが、送信ボタンを SubmitButton のような別コンポーネントに切り出し、その中で useFormStatus を呼ぶ書き方です。そのボタンを親の <form> の内側に置けば、ボタンはフォームの子孫になり、正しく送信状態を受け取れます。

基本の使い方:送信中はボタンを無効化する

もっとも典型的な使い方は、送信中に送信ボタンを disabled にして、ラベルを「送信中…」に変えるものです。まずは、フォームの中に置く送信ボタンを独立したコンポーネントとして定義します。

SubmitButton.tsx
'use client';

import { useFormStatus } from 'react-dom';

export function SubmitButton() {
  // 親フォームの送信状態を取得する
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? '送信中...' : '送信する'}
    </button>
  );
}

次に、この SubmitButton<form> の内側に置きます。フォームの action には、送信時に実行される関数(Server Action や非同期関数)を渡します。

ContactForm.tsx
import { SubmitButton } from './SubmitButton';
import { sendMessage } from './actions';

export function ContactForm() {
  return (
    <form action={sendMessage}>
      <input type="text" name="name" placeholder="お名前" />
      <textarea name="message" placeholder="メッセージ" />

      {/* SubmitButton は form の子孫なので送信状態を取得できる */}
      <SubmitButton />
    </form>
  );
}

SubmitButton<form> の中に置かれているため、フォームの子孫として送信状態を受け取れます。送信が始まると pendingtrue になり、ボタンが無効化されてラベルが「送信中…」に切り替わります。処理が完了すれば自動的に false に戻り、元の表示に戻ります。二重送信を防ぎつつ、ユーザーに処理中であることを伝えられる、実用的なパターンです。

戻り値のプロパティ

useFormStatus は、親フォームの送信状態をまとめたオブジェクトを返します。各プロパティの意味は次のとおりです。

プロパティ説明
pendingフォームが送信中なら true、そうでなければ false になる真偽値。ボタンの無効化やラベル切り替えの判定に使う。
data送信中のフォームが持つ FormData オブジェクト。送信していないときは null。送信された入力値を参照できる。
methodフォームの送信に使われる HTTP メソッド。'get' または 'post' の文字列。
actionフォームの action に渡された関数への参照。action に URL 文字列が指定されている場合は null

もっとも使う頻度が高いのは pending です。data を使うと、送信中に「何を送っているか」を表示するといった応用もできます。たとえば data.get('name') のように、送信されようとしている入力値を取り出して、送信中のプレビューに使えます。

useActionState との違いと使い分け

フォーム関連のフックには、似た名前の useActionState もあります。両者は目的が異なるので、混同しないように整理しておきましょう。

useActionStatereact が提供するフックで、フォームのアクションを実行し、その「結果の state」を保持するためのものです。たとえば送信後のエラーメッセージや、更新後の値といったアクションの返り値を state として受け取れます。呼び出した結果として、現在の state・ラップされた action・送信中かどうかを示す isPending の3つが返ります。こちらはフォームを定義する側のコンポーネントで使うのが基本です。

一方 useFormStatus は、アクションの結果には関与せず、あくまで「送信中かどうか」という状態だけを、フォームの子コンポーネントから取得するためのものです。送信ボタンのように、フォーム本体とは別のコンポーネントで送信状態を知りたいときに向いています。ざっくり言えば、アクションの結果(成功・失敗・返り値)を扱いたいなら useActionState、送信中の見た目だけを子から制御したいなら useFormStatus、という使い分けになります。両者を組み合わせて、フォーム側で useActionState の結果を表示しつつ、ボタン側で useFormStatus によって送信中の表示を切り替える、という使い方もよく行われます。

pending が常に false のままになるとき

useFormStatus を使っていて「送信中なのに pending が変わらない」というときは、いくつか決まった原因があります。順番に確認していきましょう。

form と同じコンポーネントで呼んでいる

もっとも多い原因がこれです。前述のとおり、useFormStatus は自分より上位にある <form> の状態を参照します。<form> を書いているコンポーネントと同じ場所で呼ぶと、そのフォームは上位に存在しないため状態を取得できず、pending は常に false になります。送信ボタンを SubmitButton のような別コンポーネントに切り出し、それをフォームの内側に置いて、そのコンポーネントの中で useFormStatus を呼ぶようにしてください。

Client Component になっていない

useFormStatus はフックなので、状態を扱う Client Component の中でしか使えません。Next.js の App Router のように Server Component が既定の環境では、フックを呼ぶファイルの先頭に 'use client' を付ける必要があります。これを忘れると、そもそもコンポーネントがサーバー側で扱われてフックが動きません。送信ボタンのコンポーネントには 'use client' を付けておきましょう。

ボタンが form の外側にある

コンポーネントを分けていても、そのボタンが実際には <form> の外側にレンダリングされていると、フォームの子孫にならないため状態を取得できません。レイアウトの都合でボタンをフォームの外に置きたくなることもありますが、useFormStatus を使う場合は必ず対象のフォームの内側に配置してください。DOM 上の親子関係ではなく、React のツリー上でフォームの子孫になっている必要があります。

まとめ

useFormStatusreact-dom が提供するフックで、親の <form> の送信状態を、その内側にある子コンポーネントから取得できます。戻り値の pending を使えば、送信中に送信ボタンを無効化して「送信中…」と表示する、といった処理を props のバケツリレーなしで実現できます。このほか datamethodaction といった情報も取得できます。最大の注意点は、<form> と同じコンポーネントで呼んでも状態が取れないことです。送信ボタンを SubmitButton のような別コンポーネントに切り出し、それをフォームの内側に置いて使うのが定石です。アクションの結果の state まで扱いたいときは useActionState を、送信中の見た目だけを子から制御したいときは useFormStatus を、と目的で使い分けましょう。

参考ページ