React・外のデータとつなぐ

フォームでデータを増やす

入力欄が3つあるフォームを作り、送信されたら図鑑に鳥を1羽足します。複数のstate、onSubmitとpreventDefault、入力チェック、送信後のリセット、そして新しいデータに付けるidの決め方まで扱います。

前回でデータが外の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>}
べんりワザ入力チェック(バリデーション)の基本ブラウザ標準の入力チェック(requiredtype="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をセットにする理由が書かれています

このレッスンのまとめ

  1. 入力欄が増えたら、stateも同じ数だけでよい(数個なら、それがいちばん読みやすい)
  2. <select><select>側のvalueで選択を決める(HTMLのselectedではない)
  3. 送信はonSubmitで受ける——Enterキーとブラウザの入力チェックが効く
  4. preventDefault()を忘れるとページが再読みこみされる
  5. 入力チェックは早期return、メッセージはstate+条件つき表示
  6. 送信後はstateを初期値に戻すだけで入力欄が空になる
  7. 要素そのものを操作したいときはuseRef(フォーカスなど)。refを変えても再描画は起きない
  8. 新しいデータのidはcrypto.randomUUID()。配列の長さは使わない

次は、たまってきた処理をカスタムフックにまとめます。お気に入りがリロードしても消えないようにする回です。

やってみよう:鳥を追加するフォームを作る

AddBirdFormを作って、図鑑に鳥を足せるようにします。

  1. src/components/AddBirdForm.jsxを作り、下のコードを書く
  2. App.jsxaddBird関数を足す
  3. 一覧の上に<AddBirdForm onAdd={addBird} />を置く
  4. 名前を空のまま送信して、エラーメッセージが出ることを確認する
import { useState } from 'react'

function AddBirdForm({ onAdd }) {
  const [name, setName] = useState('')
  const [area, setArea] = useState('')
  const [emoji, setEmoji] = useState('🐧')
  const [error, setError] = useState('')

  function handleSubmit(event) {
    event.preventDefault()

    if (name.trim() === '') {
      setError('名前を入れてください')
      return
    }

    onAdd({
      id: crypto.randomUUID(),
      name: name.trim(),
      area: area.trim() || 'ひみつ',
      emoji,
      rare: 1,
      memo: '',
    })

    setName('')
    setArea('')
    setError('')
  }

  return (
    <form
      onSubmit={handleSubmit}
      className="mt-6 flex flex-wrap items-end gap-3 rounded-2xl bg-white p-5 shadow-sm"
    >
      <label className="text-sm text-slate-600">
        名前
        <input
          type="text"
          value={name}
          onChange={(event) => setName(event.target.value)}
          className="mt-1 block w-44 rounded-lg border border-sky-200 px-3 py-1.5"
        />
      </label>
      <label className="text-sm text-slate-600">
        すんでいる場所
        <input
          type="text"
          value={area}
          onChange={(event) => setArea(event.target.value)}
          className="mt-1 block w-44 rounded-lg border border-sky-200 px-3 py-1.5"
        />
      </label>
      <label className="text-sm text-slate-600">
        アイコン
        <select
          value={emoji}
          onChange={(event) => setEmoji(event.target.value)}
          className="mt-1 block rounded-lg border border-sky-200 px-3 py-1.5"
        >
          <option>🐧</option>
          <option>🦜</option>
          <option>🦩</option>
          <option>🐓</option>
        </select>
      </label>
      <button
        type="submit"
        className="rounded-full bg-sky-600 px-4 py-2 text-sm font-bold text-white"
      >
        ずかんに追加
      </button>
      {error && <p className="w-full text-sm text-red-600">{error}</p>}
    </form>
  )
}

export default AddBirdForm
import AddBirdForm from './components/AddBirdForm.jsx'

function addBird(newBird) {
  setBirds((current) => [...current, newBird])
}
ことりずかんの上部にフォームがあり、名前欄に「ペンギン」、すんでいる場所欄に「なんきょく」と入力されている
送信する前。入力した文字は全部stateに入っています
9羽目としてペンギンのカードが追加され、上の件数が「9羽みつかりました」に変わっている。フォームの入力欄は空に戻っている
追加のあと。件数もカードも同時に増え、フォームは空に戻ります

「ペンギン」と入れて追加したあと、検索ボックスに「ペン」と打つと、追加した鳥もちゃんと検索に引っかかります。データが1か所にまとまっている効果です。

よくある質問

入力欄が増えたら、stateも同じ数だけ作るのですか?
数個ならそれがいちばん読みやすい書き方です。10個を超えるような大きなフォームでは、1つのオブジェクトにまとめたり、フォーム専用のライブラリを使ったりします。
onSubmitとボタンのonClick、どちらを使うべきですか?
フォームならonSubmitです。Enterキーでの送信も拾えますし、ブラウザの入力チェックも働きます。onClickだとキーボードだけで操作する人が送信できなくなります。
新しいデータのidはどう決めればいいですか?
重複しない値なら何でも構いません。ブラウザにあるcrypto.randomUUID()が手軽です。配列の長さ(birds.length + 1)は、削除が起きると重複するので避けてください。
追加した鳥がリロードで消えてしまいます
stateはメモリ上の値なので、リロードすると初期状態に戻ります。残したい場合は次の回のlocalStorageや、サーバー側に保存する仕組みが必要です。