前回でデータが外のJSONから届くようになりました。今回は逆に、画面からデータを増やします。
図鑑に「見つけた鳥」を自分で足せるフォームを作ります。
入力欄が3つ=stateも3つ
前に作った検索ボックスと同じ形を、そのまま数だけ増やします。
const [name, setName] = useState('')
const [area, setArea] = useState('')
const [emoji, setEmoji] = useState('🐧')「stateが増えて面倒では?」と思うかもしれませんが、数個ならこれがいちばん読みやすい書き方です。どの入力がどのstateに対応しているかが一目で分かります。
<select>も同じ「制御コンポーネント」の形で書けます。
<select value={emoji} onChange={(event) => setEmoji(event.target.value)}>
<option>🐧</option>
<option>🦜</option>
</select>HTMLでは選択中の<option>にselectedを付けましたが、Reactでは<select>側のvalueで決めます。ここはHTMLとの違いです。
送信はonSubmitで受ける
ボタンのonClickではなく、<form>のonSubmitで受けます。
<form onSubmit={handleSubmit}>
…
<button type="submit">ずかんに追加</button>
</form>onSubmitにする理由は2つあります。
- Enterキーでも送信できる——入力欄でEnterを押したときも同じ処理が走る
- ブラウザの入力チェックが働く——
requiredなどの標準機能がそのまま使える
入力チェックは「早期return」で
前に出てきた早期returnが、ここでも効きます。
function handleSubmit(event) {
event.preventDefault()
if (name.trim() === '') {
setError('名前を入れてください')
return // ← ここで打ち切る
}
onAdd({ … })
setName('')
setArea('')
setError('')
}trim()は前後の空白を落とすメソッドです。スペースだけの入力を弾くために挟んでいます。
エラーメッセージもstateで持ち、条件つき表示で出します。
{error && <p className="text-sm text-red-600">{error}</p>}べんりワザ入力チェック(バリデーション)の基本ブラウザ標準の入力チェック(required・type="email"・reportValidity())はこちらに送信できたら空に戻す
setName('')のようにstateを初期値に戻すだけで、入力欄が空になります。
「入力欄を空にする」のではなく「stateを空にすれば、画面がそれに従う」——制御コンポーネントにしておいた効果がここで出ます。
送信のあと、入力欄にカーソルを戻す(useRef)
続けて何羽も登録するなら、送信のあと名前の欄にカーソルが戻っていると快適です。これはstateでは書けません。「入力欄そのものを操作したい」=DOMの要素を直接触りたいからです。
そのための道具がuseRefです。
import { useRef, useState } from 'react'
const nameInputRef = useRef(null)
// 送信の最後で
nameInputRef.current.focus()
// 入力欄につなぐ
<input type="text" ref={nameInputRef} value={name} … />useRef(null)で入れ物を作るref={…}でその入れ物を要素につなぐ.currentが実物のDOM要素(focus()などが呼べる)
追加する側:新しいidを付ける
親(App)側は、受け取ったデータを配列に足すだけです。
function addBird(newBird) {
setBirds((current) => [...current, newBird])
}元の配列をこわさずコピーを作る——ここも今までどおりです。
問題はidです。keyに使うので、重複しない値でなければいけません。
id: crypto.randomUUID() // ○ ブラウザが作るランダムな文字列
id: birds.length + 1 // ✕ 1件消すと重複するcrypto.randomUUID()はブラウザに用意されている機能で、呼ぶたびに違う文字列を返します。追加したデータをサーバーに保存する場合は、サーバー側が付けたidを使うのが普通です。
部品として切り出す
フォームは入力欄・state・送信処理を一式持つので、そのままApp.jsxに書くと一気に読みにくくなります。前に学んだとおり、名前が付くまとまりなので切り出します。
AddBirdForm(フォームの中身とstate)
↓ onAdd(新しい鳥) を呼ぶ
App(配列を持っている)親はonAddという関数を渡すだけ。値は下へ、知らせは関数で上へ——「状態の持ち上げ」の回と同じ形です。
公式ドキュメントも見てみる
ja.react.dev外部サイト<input> – React公式の<input>のリファレンス。「フォームを制御されたコンポーネントとして扱う」の節に、valueとonChangeをセットにする理由が書かれていますこのレッスンのまとめ
- 入力欄が増えたら、stateも同じ数だけでよい(数個なら、それがいちばん読みやすい)
<select>は<select>側のvalueで選択を決める(HTMLのselectedではない)- 送信は
onSubmitで受ける——Enterキーとブラウザの入力チェックが効く preventDefault()を忘れるとページが再読みこみされる- 入力チェックは早期return、メッセージはstate+条件つき表示
- 送信後はstateを初期値に戻すだけで入力欄が空になる
- 要素そのものを操作したいときは
useRef(フォーカスなど)。refを変えても再描画は起きない - 新しいデータのidは
crypto.randomUUID()。配列の長さは使わない
次は、たまってきた処理をカスタムフックにまとめます。お気に入りがリロードしても消えないようにする回です。

