新規登録画面にフォームを作ろう

← 第7
Reactアプリ作成(JS編)8 / 15
9回 →

今回やること

前回は、コンポーネントや初期データを 役割ごとのファイルへ整理しました。

今回は、CreateScreenに 名前・カテゴリ・ステータス・価格・メモを入力するフォームを作ります。
入力途中の値はStateで管理し、 入力内容が変わったときにStateも更新できるようにします。

最後に、入力内容を newItemという新しいデータにまとめ、 Consoleで確認します。

気になるものリストの新規登録画面に入力フォームを表示した状態
今回の完成イメージ:新規登録画面の入力フォーム
スポンサーリンク

1. フォームの入力値をStateで管理しよう

今回はCreateScreenの中でStateを使うため、 まずuseStateを読み込みます。

CreateScreen.jsxの先頭に追加します。

import { useState } from "react";

次に、フォームに最初から入れておく値を用意します。
CreateScreen.jsxのimport文の下、 CreateScreenコンポーネントの外側に initialInputValuesを追加してください。

const initialInputValues = {
  name: "",
  category: "本",
  status: "気になる",
  price: "",
  memo: "",
};

名前・価格・メモは空の状態から始め、 カテゴリとステータスには最初から表示する値を入れています。

続いて、CreateScreenコンポーネントの中に Stateを追加します。

const [inputValues, setInputValues] = useState(initialInputValues);

inputValuesが現在の入力値を保持するstate変数、 setInputValuesがStateを更新するための更新関数です。
Stateの値には、名前・カテゴリ・ステータス・価格・メモを持つ 1つのオブジェクトを使っています。

2. まずは名前の入力欄を作ろう

いきなり5つすべての入力欄を作るのではなく、 まずは名前だけを作ってみましょう。

CreateScreenh2の下に formを追加し、 その中に名前の入力欄を作ります。

<form>
  <label>
    名前
    <input value={inputValues.name} />
  </label>
</form>

value={inputValues.name}によって、 inputに表示する値をstate変数 inputValuesnameと結び付けています。

文字を入力できないことを確認しよう

ファイルを保存してブラウザを開き、 名前の入力欄へ文字を入力してみましょう。
この状態では、文字を入力しようとしても入力内容は変わりません。

inputに表示される内容が inputValues.nameによって決まっているためです。
文字を入力したときに、 State側の値も更新する必要があります。

3. onChangeで入力値の変更を受け取ろう

文字を入力したときにStateも更新できるように、 名前の入力欄へonChangeを追加します。
先ほど追加したformの部分を、次のように書き換えてください。

<form>
  <label>
    名前
    <input
      value={inputValues.name}
      onChange={(event) => {
        setInputValues({
          ...inputValues,
          name: event.target.value,
        });
      }}
    />
  </label>
</form>

名前を入力できることを確認しよう

ファイルを保存してブラウザを確認し、 名前の入力欄へ文字を入力してみましょう。
今度は入力した文字が表示されればOKです。

新規登録画面で名前を入力
新規登録画面で名前を入力しています。

onChangeの仕組み

onChangeを追加すると入力できるようになりました。
ここで、先ほど追加した処理を順番に確認しましょう。

onChange

onChange={...}

onChangeは、 入力内容が変わったときに処理を実行するために使います。

eventから入力された値を取得する

event.target.value

入力内容が変わると、 そのときの情報がeventとして渡されます。
event.targetは今回変更されたinputを表し、 そのvalueから現在入力されている文字を取得できます。

新しいオブジェクトでStateを更新する

setInputValues({
  ...inputValues,
  name: event.target.value,
});

setInputValuesを使って、 入力された文字でStateを更新します。

...inputValuesで現在の入力値をコピーし、 nameだけを新しい入力値に変更しています。

文字を入力する
↓
onChangeが実行される
↓
event.target.valueで入力値を取得する
↓
現在のinputValuesをコピーする
↓
nameを新しい値に変更する
↓
setInputValuesでStateを更新する
↓
新しい値がinputに表示される

4. handleChangeにまとめよう

名前の入力欄は動くようになりました。
ただし、このままカテゴリ・ステータス・価格・メモにも 同じようなonChangeを書くと、 似た処理を何度も書くことになります。

そこで、どの入力欄が変更された場合でも使えるように、 入力値を更新する処理をhandleChangeへまとめます。

CreateScreenコンポーネントの中で、 Stateの下に次の関数を追加してください。

const handleChange = (event) => {
  const fieldName = event.target.name;
  const value = event.target.value;

  const newInputValues = { ...inputValues };
  newInputValues[fieldName] = value;

  setInputValues(newInputValues);
};

handleChangeでは、 入力欄のname属性を使って どの項目を更新するかを切り替えます。

入力欄にname属性を追加する

まず、名前の入力欄へ name="name"を追加し、 onChangeにはhandleChangeを指定します。

<label>
  名前
  <input
    name="name"
    value={inputValues.name}
    onChange={handleChange}
  />
</label>

name属性によって、 この入力欄がinputValuesnameを変更する入力欄だと区別できるようになります。

変更する項目を取得する

const fieldName = event.target.name;

event.target.nameから、 変更された入力欄のname属性を取得します。

名前の入力欄ではname="name"なので、 fieldNameには"name"が入ります。

fieldName → "name"

入力された値は、先ほどと同じように event.target.valueから取得します。

const value = event.target.value;

fieldNameを使って変更する項目を切り替える

const newInputValues = { ...inputValues };
newInputValues[fieldName] = value;

newInputValuesを作る考え方は、 先ほどのState更新と同じです。
今回は、fieldNameをプロパティ名として使うことで、 変更する項目を切り替えています。

名前の入力欄ならfieldName"name"なので、実際には次の処理になります。

newInputValues["name"] = value;

カテゴリの入力欄にname="category"を付ければ、 fieldNameには"category"が入り、 同じhandleChangecategoryを更新できます。

名前を変更
↓
fieldName → "name"
↓
newInputValues["name"]を更新

カテゴリを変更
↓
fieldName → "category"
↓
newInputValues["category"]を更新

最後に、変更したnewInputValuesで Stateを更新します。

setInputValues(newInputValues);

これで、入力欄ごとに別々の処理を書くのではなく、 1つのhandleChangeで入力値を更新できるようになりました。

5. ほかの入力欄も追加しよう

名前の入力欄を作れたので、 残りの入力欄も追加していきましょう。

選択肢を読み込む

カテゴリとステータスでは、 前回options.jsに用意した選択肢を使います。

CreateScreen.jsxの先頭に、 次のimport文を追加してください。

import { categories, statuses } from "../options";

CreateScreen.jsxcomponentsフォルダの中にあり、 options.jsはその1つ上のsrcにあります。
そのため、1つ上のフォルダを表す ../を使って読み込んでいます。

カテゴリとステータスを追加する

名前の入力欄の下に、 カテゴリとステータスのselectを追加します。

<label>
    カテゴリ
    <select
      name="category"
      value={inputValues.category}
      onChange={handleChange}
    >
      {categories.map((value) => (
        <option key={value} value={value}>
          {value}
        </option>
      ))}
    </select>
  </label>

  <label>
    ステータス
    <select
      name="status"
      value={inputValues.status}
      onChange={handleChange}
    >
      {statuses.map((value) => (
        <option key={value} value={value}>
          {value}
        </option>
      ))}
    </select>
  </label>

categoriesstatusesを それぞれmap()で処理し、 1つずつoptionとして表示しています。
map()keyの考え方は、 一覧表示のときと同じです。

また、name="category"name="status"を付けているため、 どちらも同じhandleChangeで更新できます。

価格とメモを追加する

続いて、価格とメモを ステータスの入力欄の下に追加します。

<label>
    価格
    <input
      name="price"
      type="number"
      min="0"
      value={inputValues.price}
      onChange={handleChange}
    />
  </label>

  <label>
    メモ
    <textarea
      name="memo"
      value={inputValues.memo}
      onChange={handleChange}
    />
  </label>

価格にはtype="number"、 メモには複数行入力できるtextareaを使います。

5つの入力項目を確認しよう

ファイルを保存して、 名前・カテゴリ・ステータス・価格・メモを それぞれ入力・変更できることを確認しましょう。

名前、カテゴリ、ステータス、価格、メモを入力できる新規登録フォーム
新規登録画面に5つの入力項目が表示されています。

6. 登録ボタンと送信処理を作ろう

入力欄がそろったので、 フォームの最後に「登録する」ボタンを追加し、 送信時の処理も作りましょう。

登録ボタンを追加する

formの最後に、次のボタンを追加します。

<button type="submit">
  登録する
</button>

type="submit"を指定すると、 このボタンはフォームを送信するためのボタンになります。

フォームの送信処理を作る

フォームが送信されたときの処理は、 onSubmitで受け取ります。

これまで使ってきたformに、 onSubmitを追加します。

<form onSubmit={handleSubmit}>

次に、CreateScreenコンポーネントの中へ handleSubmitを追加します。

const handleSubmit = (event) => {
  event.preventDefault();

  console.log(inputValues);
};

HTMLのformには、本来のフォーム送信処理があります。
フォームを通常どおり送信すると、 ブラウザは指定された送信先へデータを送り、 ページの移動や再読み込みを行います。

今回は、その標準の送信処理ではなく、 React側のJavaScriptで入力内容を処理したいので、 event.preventDefault()を使います。
preventDefault()は、 イベントが本来行う標準の動作を止めるためのメソッドです。

「登録する」を押す
↓
formのonSubmitが実行される
↓
handleSubmitが呼ばれる
↓
preventDefault()で通常のフォーム送信を止める
↓
JavaScriptで入力内容を処理する

Consoleで入力内容を確認しよう

名前や価格、メモなどを入力して「登録する」を押し、 ブラウザの開発者ツールでConsoleタブを確認しましょう。
入力した内容を持つオブジェクトが表示されれば、 フォームの値をStateで正しく管理し、送信時に取得できています。

開発者ツールのConsoleに表示される入力データ
Consoleに入力データが表示されています。

このconsole.log(inputValues)は、 次のセクションでnewItemを確認するコードへ変更します。

7. 登録するデータを作ろう

入力内容を取得できたので、 登録する1件分のデータを作りましょう。

今回は、名前を必須にし、名前の前後にある空白を取り除きます。
また、inputから取得した価格は文字列として扱われるため、 数値へ変換してから登録します。

名前の入力を必須にする

名前のinputにrequiredを追加してください。

<label>
  名前
  <input
    name="name"
    value={inputValues.name}
    onChange={handleChange}
    required
  />
</label>

requiredを付けると、 名前が未入力のままフォームを送信しようとしたときに、 ブラウザが入力を求めてくれます。

入力内容をnewItemにまとめる

登録するデータは、 newItemという新しいオブジェクトにまとめます。

const newItem = {
  ...inputValues,
  name: inputValues.name.trim(),
  price: Number(inputValues.price) || 0,
};
  • ...inputValues:現在の入力内容をコピーする
  • trim():名前の前後にある空白を取り除く
  • Number():価格を数値へ変換する

価格が未入力の場合は、今回は0として扱います。

作ったデータをConsoleで確認する

handleSubmitを次のように変更します。

const handleSubmit = (event) => {
  event.preventDefault();

  const newItem = {
    ...inputValues,
    name: inputValues.name.trim(),
    price: Number(inputValues.price) || 0,
  };

  console.log(newItem);
};

newItemを確認しよう

フォームに値を入力して「登録する」を押し、 Consoleを確認してください。
入力した内容を持つnewItemが表示されればOKです。

これで、新規登録画面で入力した内容を 登録する1件分のデータとしてまとめられるようになりました。

ReactとJavaScriptを分けて見てみよう

今回登場した内容を、 ReactとJavaScriptに分けて整理します。

今回登場したReactとJavaScriptの要素
種類 今回登場したもの
React State、state変数、更新関数、valueonChangeonSubmit
JavaScript オブジェクト、スプレッド構文、trim()Number()preventDefault()

フォームの入力値をStateと結び付けたり、 onChangeonSubmitで処理したりする部分は、 Reactでよく使う書き方です。
一方、オブジェクトをコピーしたり、 文字列や数値を整えたりする部分はJavaScriptです。

今回のまとめ

  • フォームの入力値を1つのStateで管理した
  • inputValuesをstate変数、setInputValuesを更新関数として使った
  • valueで入力欄とStateの値を結び付けた
  • onChangeで入力内容の変更を受け取った
  • event.target.valueで現在の入力値を取得した
  • Stateのオブジェクトをコピーしてから更新した
  • name属性で入力欄を区別した
  • event.target.nameで変更された項目を取得した
  • handleChangeで複数の入力欄の更新処理を共通化した
  • categoriesstatusesから選択肢を表示した
  • onSubmitでフォーム送信を受け取った
  • event.preventDefault()でフォームの標準送信を止めた
  • 入力内容からnewItemを作った

今回の完成コード

今回変更したCreateScreen.jsxの完成コードです。

src/components/CreateScreen.jsx

import { useState } from "react";

import { categories, statuses } from "../options";

const initialInputValues = {
  name: "",
  category: "本",
  status: "気になる",
  price: "",
  memo: "",
};

export default function CreateScreen({ onBack }) {
  const [inputValues, setInputValues] = useState(initialInputValues);

  const handleChange = (event) => {
    const fieldName = event.target.name;
    const value = event.target.value;

    const newInputValues = { ...inputValues };
    newInputValues[fieldName] = value;

    setInputValues(newInputValues);
  };

  const handleSubmit = (event) => {
    event.preventDefault();

    const newItem = {
      ...inputValues,
      name: inputValues.name.trim(),
      price: Number(inputValues.price) || 0,
    };

    console.log(newItem);
  };

  return (
    <main className="form-screen">
      <header className="screen-header">
        <h1>気になるものリスト</h1>
        <div className="header-actions">
          <button type="button" className="secondary" onClick={onBack}>
            一覧へ
          </button>
        </div>
      </header>

      <h2 className="screen-title">新規登録画面</h2>

      <form onSubmit={handleSubmit}>
        <label>
          名前
          <input
            name="name"
            value={inputValues.name}
            onChange={handleChange}
            required
          />
        </label>

        <label>
          カテゴリ
          <select
            name="category"
            value={inputValues.category}
            onChange={handleChange}
          >
            {categories.map((value) => (
              <option key={value} value={value}>
                {value}
              </option>
            ))}
          </select>
        </label>

        <label>
          ステータス
          <select
            name="status"
            value={inputValues.status}
            onChange={handleChange}
          >
            {statuses.map((value) => (
              <option key={value} value={value}>
                {value}
              </option>
            ))}
          </select>
        </label>

        <label>
          価格
          <input
            name="price"
            type="number"
            min="0"
            value={inputValues.price}
            onChange={handleChange}
          />
        </label>

        <label>
          メモ
          <textarea
            name="memo"
            value={inputValues.memo}
            onChange={handleChange}
          />
        </label>

        <button type="submit">
          登録する
        </button>
      </form>
    </main>
  );
}

アプリの完成状況

  • ✅ Reactを動かし、JSXを表示する
  • ✅ Stateで画面を切り替える
  • ✅ 1件のデータを表示する
  • ✅ 複数のデータを一覧表示する
  • ✅ コンポーネントとpropsを使う
  • ✅ コンポーネントを別ファイルへ分ける
  • ✅ 新規登録画面を作る
  • ⬜ 一覧データをStateで管理する
  • ⬜ 編集画面を作る
  • ⬜ データを更新する
  • ⬜ データを削除する
  • ⬜ カテゴリ・ステータスで絞り込む
  • ⬜ キーワードで検索する
  • ⬜ localStorageで保存して完成
スポンサーリンク