3.8 フォーム — 制御コンポーネント
ここで理解してほしいのは、「React では入力要素の値を React の state が持つ か、DOM が持つ かの二者択一である」という一点です。前者を 制御コンポーネント (controlled)、後者を 非制御コンポーネント (uncontrolled) と呼びます。どちらを選ぶかで書き方が大きく変わるので、最初のうちに違いを押さえておきましょう。
制御コンポーネント — state が真実の源
制御コンポーネントは、入力要素に value と onChange を両方与えます。値は常に React の state と一致します。
import { useState } from 'react'
export default function NameForm() {
const [name, setName] = useState('')
return (
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
)
}公式の仕様記述はこうです。
To render a controlled input, pass the
valueprop to it (orcheckedfor checkboxes and radios). React will force the input to always have thevalueyou passed.
「React が value を強制する」のがポイントです。ユーザーがキーを押した瞬間に onChange が走り、setName で state が更新され、次のレンダーで <input value={name}> が反映されます。state を更新しない限り、入力欄に文字は入りません。
図 3.8.1 制御コンポーネントでの 1 キー分の流れ
onChange を書き忘れるとどうなるか
公式は明確に警告しています。
If you pass
valuewithoutonChange, it will be impossible to type into the input.出典: 同上
さらに、React は開発時のコンソールにエラーを出します。
You provided a
valueprop to a form field without anonChangehandler. This will render a read-only field.出典: 同上
読み取り専用にしたいなら readOnly を付けるのが正解です。それ以外は必ず onChange を一緒に書きます。
非制御コンポーネント — DOM が真実の源
逆に、defaultValue だけを与えた入力要素は、DOM がそのまま値を管理します。React は初期値を設定するだけで、それ以降は関知しません。
<input type="text" defaultValue="初期値" />An input like
<input />is uncontrolled. Even if you pass an initial value like<input defaultValue="Initial text" />, your JSX only specifies the initial value. It does not control what the value should be right now.出典: 同上
値が必要になったタイミング (送信時など) で、ref などを使って DOM から読み出します。ref については Part 4.5 で扱います。
触って違いを実感してみる
2 つの書き方を並べたデモです。左 (制御) は「入力中に大文字化する」のような変換をリアクティブに効かせられます。右 (非制御) は、「送信ボタン相当」を押したときに初めて React 側に値が届くのが確認できます。
value + onChange"" state が「唯一の真実」。ユーザーの 1 キー入力ごとに state が更新され、 再レンダーで input の value が反映されます。だから入力中の変換もリアクティブに効きます。
defaultValue のみ(まだ読んでいません) React は初期値を与えるだけで、以降は DOM に任せます。 値が必要になったタイミング(送信時など)で ref を使って読み出します。 入力中の変換や検証はやりにくくなります。
このデモで最も大事なのは、左側は state が入力の「唯一の真実」になっている ことです。state をいじると画面が変わり、画面をいじると state が変わる、という往復が常に成立します。右側はこの往復がなく、値が必要になった時だけ DOM に問い合わせに行く形になります。
使い分けの指針
| 制御 | 非制御 | |
|---|---|---|
| state | React が持つ | DOM が持つ |
value / onChange | 両方必要 | 使わない (defaultValue のみ) |
| バリデーション・整形 | 入力中にリアクティブに行える | 送信時にまとめて読む |
| お勧めの場面 | 入力に応じて UI が変わる・検証をリアルタイムに出す | 単純なフォームで送信時しか値を使わない |
実務では制御コンポーネントが主流です。本章もこれを前提とします。
フォームの送信 — 伝統的な onSubmit
<form> の送信は onSubmit でハンドリングします。ブラウザの既定動作 (ページ遷移) を止めるために e.preventDefault() を呼びます。
import { useState } from 'react'
export default function SignupForm() {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [error, setError] = useState('')
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
if (!name) {
setError('名前を入力してください')
return
}
if (!email.includes('@')) {
setError('メールアドレスの形式が不正です')
return
}
setError('')
// 送信処理 (API コール等)
console.log({ name, email })
}
return (
<form onSubmit={handleSubmit}>
<label>
名前
<input value={name} onChange={(e) => setName(e.target.value)} />
</label>
<label>
メール
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</label>
{error && <p role="alert">{error}</p>}
<button type="submit">送信</button>
</form>
)
}入力中にエラーを出すか、送信時にまとめて出すかはチームの好みで選びます。制御コンポーネントなら、どちらも同じように書けます。
useId で label と input を結ぶ
アクセシビリティのため、<label htmlFor="..."> と <input id="..."> は対にしておきます。コンポーネントが複数箇所で使われても一意な ID を作るには、useId フックを使います。
import { useId, useState } from 'react'
export default function EmailField() {
const id = useId()
const [email, setEmail] = useState('')
return (
<div>
<label htmlFor={id}>メールアドレス</label>
<input
id={id}
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
)
}公式はこう定義しています。
useId returns a unique ID string associated with this particular useId call in this particular component.
手書きで id="email" と書くと、同じコンポーネントを 2 個置いた時に id が衝突します。useId を使えばその心配がありません。
React 19 の <form action={fn}>
React 19 からは <form> の action 属性に 関数 を渡せるようになりました。
function search(formData: FormData) {
const query = formData.get('query')
console.log('検索:', query)
}
export default function SearchForm() {
return (
<form action={search}>
<input name="query" />
<button type="submit">検索</button>
</form>
)
}公式の挙動説明はこうです。
When a function is passed to
action, the function will handle the form submission in a Transition following the Action prop pattern. The function passed toactionmay be async and will be called with a single argument containing the form data of the submitted form.
従来の onSubmit との違いは次の点です。
- 非同期関数が書ける — Promise を返せる
- Transition の中で実行される — ペンディング状態を自動管理できる
preventDefault()不要 — React が面倒を見る- 送信後に非制御の入力が自動リセット — "After the
actionfunction succeeds, all uncontrolled field elements in the form are reset."
Transition、useActionState、useOptimistic といった Actions 関連の詳細は Part 4.2 で扱います。Part 3 の範囲では「action={fn} という書き方がある」ことだけ押さえて先に進みましょう。
まとめ
- 入力要素の値を React の state が持つ (制御) か、DOM が持つ (非制御) かを決める。
- 制御コンポーネントは
value+onChangeがセット。valueだけ渡すと読み取り専用になる。 - 伝統的な
onSubmitハンドラではe.preventDefault()を必ず呼ぶ。 useIdで label と input の id を安全に結ぶ。- React 19 の
<form action={fn}>は Actions の入口。詳細は Part 4。
さらに深く学ぶには
- react.dev — input Reference — 制御 / 非制御の正確な挙動。
- react.dev — form Reference — action 関数の仕様。
- react.dev — useId Reference — アクセシビリティ用 id の生成。
- 4.2 use / useOptimistic / useActionState — Actions で書くフォーム。