[React講座] フォームとユーザー入力

当ページのリンクには広告が含まれています。
目次

フォームとユーザー入力

Controlled Component とは何か?

Reactでフォームを扱うときの大原則:

入力欄の値を「DOM任せ」にせず、Reactの状態で管理する。

このパターンを Controlled Component(制御されたコンポーネント) と呼びます。

例:

import { useState } from 'react'

export default function App() {
  const [name, setName] = useState('')

  function handleChange(e) {
    setName(e.target.value)
  }

  return (
    <div>
      <input
        type="text"
        value={name}
        onChange={handleChange}
        placeholder="名前を入力"
      />
      <p>こんにちは、{name || 'ゲスト'} さん</p>
    </div>
  )
}

ポイント整理:

  • value={name}⇒ 「このinputの中身は、常に state name の値と同期していますよ」という宣言
  • onChange={handleChange}⇒ DOM側で変化が起きたら、setNameでReact側のstateに反映
  • 結果として:「入力値 = state」 の1本線の設計になる

この設計にすると以下のようなメリットがあります。

  • 入力内容をいつでも参照できる
  • バリデーションがしやすい
  • 送信イベントで state をまとめて送れる

テキスト入力(input, textarea)の基本

単一テキスト

const [title, setTitle] = useState('')

<input
  type="text"
  value={title}
  onChange={(e) => setTitle(e.target.value)}
/>

textarea

textarea も同じノリです(子要素ではなく value 属性で制御します)。

const [body, setBody] = useState('')

<textarea
  value={body}
  onChange={(e) => setBody(e.target.value)}
  rows={4}
  cols={40}
/>

Reactでは defaultValue ではなく、基本的に value を使って常に制御するのが基本です。

select, checkbox, radio

select(単一選択)

const [country, setCountry] = useState('jp')

<select
  value={country}
  onChange={(e) => setCountry(e.target.value)}
>
  <option value="jp">日本</option>
  <option value="us">アメリカ</option>
  <option value="uk">イギリス</option>
</select>

valueonChange を見れば、もうパターンはわかると思います。

checkbox(真偽値)

const [subscribe, setSubscribe] = useState(false)

<label>
  <input
    type="checkbox"
    checked={subscribe}
    onChange={(e) => setSubscribe(e.target.checked)}
  />
  メールマガジンを受け取る
</label>
  • value ではなく checked
  • e.target.checked で真偽値が取れる

radio(排他選択)

const [plan, setPlan] = useState('free')

<label>
  <input
    type="radio"
    name="plan"
    value="free"
    checked={plan === 'free'}
    onChange={(e) => setPlan(e.target.value)}
  />
  無料プラン
</label>
<label>
  <input
    type="radio"
    name="plan"
    value="pro"
    checked={plan === 'pro'}
    onChange={(e) => setPlan(e.target.value)}
  />
  Proプラン
</label>
  • 同じグループのradioは name を揃える
  • どれが選択中かは state plan で管理し、checked={plan === 'free'} のように制御

「フォーム全体」をどうstateで持つか

フォームが大きくなると、
name, email, age, memo … と useState が増えていきます。

パターンは主に2つ:

  1. stateを項目ごとに分ける
  2. 1つのオブジェクトstateにまとめる

1) 項目ごとに分ける

const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [age, setAge] = useState('')
  • シンプルで分かりやすい
  • 項目が増えるとだるい

2) オブジェクトにまとめる

const [form, setForm] = useState({
  name: '',
  email: '',
  age: '',
})

function handleChange(e) {
  const { name, value } = e.target
  setForm((prev) => ({
    ...prev,
    [name]: value,
  }))
}
<input
  name="name"
  value={form.name}
  onChange={handleChange}
/>

<input
  name="email"
  value={form.email}
  onChange={handleChange}
/>

// など
  • input側に name 属性を付けておき、
  • 共通の handleChangeform オブジェクトに書き込む

大きめのフォームではこのオブジェクトパターンを使うことが多いです。
(もしくは react-hook-form など外部ライブラリを使うことが多くなる)

フォーム送信と onSubmit

Reactでは「送信ボタンのクリック」より、フォーム全体の onSubmit を使うのが定石です。

import { useState } from 'react'

export default function ContactForm() {
  const [form, setForm] = useState({
    name: '',
    email: '',
    message: '',
  })

  function handleChange(e) {
    const { name, value } = e.target
    setForm((prev) => ({ ...prev, [name]: value }))
  }

  function handleSubmit(e) {
    e.preventDefault()        // ブラウザのデフォルト送信(ページリロード)を防ぐ

    // ここでバリデーション&送信処理
    console.log('送信データ:', form)
    alert('送信しました!')
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>
          名前:
          <input
            name="name"
            value={form.name}
            onChange={handleChange}
          />
        </label>
      </div>

      <div>
        <label>
          メール:
          <input
            name="email"
            type="email"
            value={form.email}
            onChange={handleChange}
          />
        </label>
      </div>

      <div>
        <label>
          メッセージ:
          <textarea
            name="message"
            value={form.message}
            onChange={handleChange}
          />
        </label>
      </div>

      <button type="submit">送信</button>
    </form>
  )
}

ポイント:

  • form タグに onSubmit
  • 送信時のページリロードを防ぐために e.preventDefault() を必ず呼ぶ
  • 送信したいデータは form state からまとめて取れる

シンプルなバリデーション

まずは「送信前に自前でチェックする」パターンを押さえておくと便利です。

必須チェック & 簡単なエラー表示

const [form, setForm] = useState({ name: '', email: '' })
const [errors, setErrors] = useState({})

function validate() {
  const newErrors = {}

  if (!form.name.trim()) {
    newErrors.name = '名前は必須です'
  }

  if (!form.email.trim()) {
    newErrors.email = 'メールアドレスは必須です'
  } else if (!form.email.includes('@')) {
    newErrors.email = 'メールアドレスの形式が不正です'
  }

  setErrors(newErrors)
  return Object.keys(newErrors).length === 0
}

function handleSubmit(e) {
  e.preventDefault()
  if (!validate()) return

  // バリデーションOKなら送信処理
}

表示側:

<div>
  <label>
    名前:
    <input
      name="name"
      value={form.name}
      onChange={handleChange}
    />
  </label>
  {errors.name && <p style={{ color: 'red' }}>{errors.name}</p>}
</div>

考え方として重要なのは、「フォームの値」用のstateと、「エラーメッセージ」用のstateを分けて持つ。
という構造です。
これだけでかなり柔軟なバリデーションが書けるようになります。

Reactらしいフォームの考え方

ここまでの内容を、少し抽象化して整理すると:

  1. 状態は1か所で持つ
    • フォーム全体の状態を useState で管理
    • 入力欄は「stateの現在値を表示するだけ」
  2. イベントは上に伝える
    • onChange, onSubmit で「何が起きたか」をコンポーネントのロジックに伝える
    • 実際にどう処理するか(state更新・API送信など)は関数側で定義
  3. 「UIは state の関数」という意識
    • state が変われば、エラーメッセージの表示/非表示も自動で切り替わる
    • isSubmitting, isSuccess, isError などの状態を追加すれば、
      • ボタンをdisableにする
      • 成功メッセージを出す
      • ローディングスピナーを出す
        なども、「状態を変えるだけ」で反映できる

まとめ

  • Reactのフォームは Controlled Component が基本
    value / checkedonChange で state と完全同期
  • text・textarea・select・checkbox・radio すべて同じパターンで扱える
  • フォーム全体を1つのオブジェクトstateで持ち、共通 handleChange で更新するパターンは実務でも多用
  • onSubmit + e.preventDefault() で送信イベントをハンドリング
  • バリデーションは
    • form(値)と errors(エラー)を分ける
    • validate() 関数でチェック ⇒ OKなら送信へ

<<前へ(コンポーネント設計とデータの流れ)

>>次へ(副作用とライフサイクル(useEffect))

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次