目次
フォームとユーザー入力
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の中身は、常に statenameの値と同期していますよ」という宣言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>value と onChange を見れば、もうパターンはわかると思います。
checkbox(真偽値)
const [subscribe, setSubscribe] = useState(false)
<label>
<input
type="checkbox"
checked={subscribe}
onChange={(e) => setSubscribe(e.target.checked)}
/>
メールマガジンを受け取る
</label>valueではなくcheckede.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つ:
- stateを項目ごとに分ける
- 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属性を付けておき、 - 共通の
handleChangeでformオブジェクトに書き込む
大きめのフォームではこのオブジェクトパターンを使うことが多いです。
(もしくは 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()を必ず呼ぶ - 送信したいデータは
formstate からまとめて取れる
シンプルなバリデーション
まずは「送信前に自前でチェックする」パターンを押さえておくと便利です。
必須チェック & 簡単なエラー表示
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か所で持つ
- フォーム全体の状態を
useStateで管理 - 入力欄は「stateの現在値を表示するだけ」
- フォーム全体の状態を
- イベントは上に伝える
onChange,onSubmitで「何が起きたか」をコンポーネントのロジックに伝える- 実際にどう処理するか(state更新・API送信など)は関数側で定義
- 「UIは state の関数」という意識
stateが変われば、エラーメッセージの表示/非表示も自動で切り替わるisSubmitting,isSuccess,isErrorなどの状態を追加すれば、- ボタンをdisableにする
- 成功メッセージを出す
- ローディングスピナーを出す
なども、「状態を変えるだけ」で反映できる
まとめ
- Reactのフォームは Controlled Component が基本
⇒value/checkedとonChangeで state と完全同期 - text・textarea・select・checkbox・radio すべて同じパターンで扱える
- フォーム全体を1つのオブジェクトstateで持ち、共通
handleChangeで更新するパターンは実務でも多用 onSubmit + e.preventDefault()で送信イベントをハンドリング- バリデーションは
form(値)とerrors(エラー)を分けるvalidate()関数でチェック ⇒ OKなら送信へ
コメント