フォームを送信している間、送信ボタンを無効化して「送信中…」と表示したい、という場面はよくあります。React には、こうした「フォームの送信中かどうか」をボタン側のコンポーネントから受け取れる useFormStatus というフックが用意されています。ポイントは、この状態を親の <form> から props で渡さなくても、フォームの内側にあるコンポーネントが自分で取得できることです。この記事では、useFormStatus の基本的な使い方から、送信ボタンを別コンポーネントに切り出す理由、戻り値のプロパティ、そして useActionState との使い分けや、うまく動かないときの原因までを順番に解説します。
目次
useFormStatus は何をするフックか
useFormStatus は react-dom が提供するフックで、親にある <form> の送信状態を、そのフォームの内側にあるコンポーネントから読み取るためのものです。インポートは react ではなく react-dom から行う点に注意してください。
import { useFormStatus } from 'react-dom';
このフックが便利なのは、送信中かどうかという状態を親から子へ props で受け渡す必要がない点です。フォームの中に置かれた送信ボタンのコンポーネントが、自分自身で「今このフォームは送信中か」を知ることができます。そのため、深くネストしたボタンでも、途中のコンポーネントを経由して状態をバケツリレーする必要がありません。
送信ボタンを別コンポーネントに切り出す必要がある
useFormStatus を使ううえで最初に理解しておきたいのが、このフックは「その <form> の子孫コンポーネント」の中で呼ばなければならない、という制約です。ここを外すと状態が取れず、多くの人が最初につまずくポイントになります。
useFormStatus は、自分より上位にある一番近い <form> の状態を参照します。裏を返すと、<form> を書いているコンポーネントと同じ場所で useFormStatus を呼んでも、そのフォームは「上位」には存在しないため状態を取得できません。この場合 pending は常に false のままになります。
そこで定石となるのが、送信ボタンを SubmitButton のような別コンポーネントに切り出し、その中で useFormStatus を呼ぶ書き方です。そのボタンを親の <form> の内側に置けば、ボタンはフォームの子孫になり、正しく送信状態を受け取れます。
基本の使い方:送信中はボタンを無効化する
もっとも典型的な使い方は、送信中に送信ボタンを disabled にして、ラベルを「送信中…」に変えるものです。まずは、フォームの中に置く送信ボタンを独立したコンポーネントとして定義します。
'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 や非同期関数)を渡します。
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> の中に置かれているため、フォームの子孫として送信状態を受け取れます。送信が始まると pending が true になり、ボタンが無効化されてラベルが「送信中…」に切り替わります。処理が完了すれば自動的に false に戻り、元の表示に戻ります。二重送信を防ぎつつ、ユーザーに処理中であることを伝えられる、実用的なパターンです。
戻り値のプロパティ
useFormStatus は、親フォームの送信状態をまとめたオブジェクトを返します。各プロパティの意味は次のとおりです。
| プロパティ | 説明 |
|---|---|
pending | フォームが送信中なら true、そうでなければ false になる真偽値。ボタンの無効化やラベル切り替えの判定に使う。 |
data | 送信中のフォームが持つ FormData オブジェクト。送信していないときは null。送信された入力値を参照できる。 |
method | フォームの送信に使われる HTTP メソッド。'get' または 'post' の文字列。 |
action | フォームの action に渡された関数への参照。action に URL 文字列が指定されている場合は null。 |
もっとも使う頻度が高いのは pending です。data を使うと、送信中に「何を送っているか」を表示するといった応用もできます。たとえば data.get('name') のように、送信されようとしている入力値を取り出して、送信中のプレビューに使えます。
useActionState との違いと使い分け
フォーム関連のフックには、似た名前の useActionState もあります。両者は目的が異なるので、混同しないように整理しておきましょう。
useActionState は react が提供するフックで、フォームのアクションを実行し、その「結果の 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 のツリー上でフォームの子孫になっている必要があります。
まとめ
useFormStatus は react-dom が提供するフックで、親の <form> の送信状態を、その内側にある子コンポーネントから取得できます。戻り値の pending を使えば、送信中に送信ボタンを無効化して「送信中…」と表示する、といった処理を props のバケツリレーなしで実現できます。このほか data・method・action といった情報も取得できます。最大の注意点は、<form> と同じコンポーネントで呼んでも状態が取れないことです。送信ボタンを SubmitButton のような別コンポーネントに切り出し、それをフォームの内側に置いて使うのが定石です。アクションの結果の state まで扱いたいときは useActionState を、送信中の見た目だけを子から制御したいときは useFormStatus を、と目的で使い分けましょう。