Skip to content

3.8 フォーム — 制御コンポーネント ​

ここで理解してほしいのは、「React では入力要素の値を React の state が持つ か、DOM が持つ かの二者択一である」という一点です。前者を 制御コンポーネント (controlled)、後者を 非制御コンポーネント (uncontrolled) と呼びます。どちらを選ぶかで書き方が大きく変わるので、最初のうちに違いを押さえておきましょう。

制御コンポーネント — state が真実の源 ​

制御コンポーネントは、入力要素に value と onChange を両方与えます。値は常に React の state と一致します。

tsx
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 value prop to it (or checked for checkboxes and radios). React will force the input to always have the value you passed.

出典: react.dev — input Reference

「React が value を強制する」のがポイントです。ユーザーがキーを押した瞬間に onChange が走り、setName で state が更新され、次のレンダーで <input value={name}> が反映されます。state を更新しない限り、入力欄に文字は入りません。

図 3.8.1 制御コンポーネントでの 1 キー分の流れ ​

onChange を書き忘れるとどうなるか ​

公式は明確に警告しています。

If you pass value without onChange, it will be impossible to type into the input.

出典: 同上

さらに、React は開発時のコンソールにエラーを出します。

You provided a value prop to a form field without an onChange handler. This will render a read-only field.

出典: 同上

読み取り専用にしたいなら readOnly を付けるのが正解です。それ以外は必ず onChange を一緒に書きます。

非制御コンポーネント — DOM が真実の源 ​

逆に、defaultValue だけを与えた入力要素は、DOM がそのまま値を管理します。React は初期値を設定するだけで、それ以降は関知しません。

tsx
<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
React の state が持っている値""

state が「唯一の真実」。ユーザーの 1 キー入力ごとに state が更新され、 再レンダーで input の value が反映されます。だから入力中の変換もリアクティブに効きます。

非制御defaultValue のみ
最後に読み出した値(まだ読んでいません)

React は初期値を与えるだけで、以降は DOM に任せます。 値が必要になったタイミング(送信時など)で ref を使って読み出します。 入力中の変換や検証はやりにくくなります。

このデモで最も大事なのは、左側は state が入力の「唯一の真実」になっている ことです。state をいじると画面が変わり、画面をいじると state が変わる、という往復が常に成立します。右側はこの往復がなく、値が必要になった時だけ DOM に問い合わせに行く形になります。

使い分けの指針 ​

制御非制御
stateReact が持つDOM が持つ
value / onChange両方必要使わない (defaultValue のみ)
バリデーション・整形入力中にリアクティブに行える送信時にまとめて読む
お勧めの場面入力に応じて UI が変わる・検証をリアルタイムに出す単純なフォームで送信時しか値を使わない

実務では制御コンポーネントが主流です。本章もこれを前提とします。

フォームの送信 — 伝統的な onSubmit ​

<form> の送信は onSubmit でハンドリングします。ブラウザの既定動作 (ページ遷移) を止めるために e.preventDefault() を呼びます。

tsx
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 フックを使います。

tsx
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.

出典: react.dev — useId Reference

手書きで id="email" と書くと、同じコンポーネントを 2 個置いた時に id が衝突します。useId を使えばその心配がありません。

React 19 の <form action={fn}> ​

React 19 からは <form> の action 属性に 関数 を渡せるようになりました。

tsx
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 to action may be async and will be called with a single argument containing the form data of the submitted form.

出典: react.dev — form Reference

従来の onSubmit との違いは次の点です。

  • 非同期関数が書ける — Promise を返せる
  • Transition の中で実行される — ペンディング状態を自動管理できる
  • preventDefault() 不要 — React が面倒を見る
  • 送信後に非制御の入力が自動リセット — "After the action function 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。

さらに深く学ぶには ​

参照ソース ​