一覧データをStateで管理しよう

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

今回やること

前回は、新規登録画面のフォームに入力した内容から newItemを作りました。

今回は、そのnewItemCreateScreenから親のAppへ渡し、 実際に一覧データへ追加します。

そのために、これまで固定データとして使っていた itemsをStateで管理できるようにします。

CreateScreen
  ↓ newItemを渡す
App
  ↓ IDを付ける
itemsを更新する
  ↓
ListScreenに新しい一覧を渡す
スポンサーリンク

itemsをStateで管理する

まず、src/App.jsxを開きます。

これまでは、initialItemsを そのままListScreenへ渡していました。

<ListScreen
  items={initialItems}
  onCreate={() => setScreen("create")}
  onEdit={() => setScreen("edit")}
/>

しかし、新しいデータを追加するには、 一覧データ自体を変更できるようにする必要があります。

そこで、Appの中に items Stateを用意します。

const [items, setItems] = useState(initialItems);

itemsが現在の一覧データを保持するstate変数、 setItemsが一覧データを更新するための更新関数です。

初期値には、前回まで使っていた initialItemsを指定しています。

ListScreenへ渡す値も、 initialItemsからitemsへ変更します。

<ListScreen
  items={items}
  onCreate={() => setScreen("create")}
  onEdit={() => setScreen("edit")}
/>

この時点では一覧の見た目は変わりません。 ただし、これでsetItemsを使って 一覧データを更新できるようになりました。

newItemをCreateScreenから渡す

次に、src/components/CreateScreen.jsxを開きます。

前回のCreateScreenでは、 フォームの入力内容からnewItemを作るところまで進めました。

今回は、そのnewItemを 親のAppへ渡します。

まず、CreateScreenで propsとしてonCreateを受け取ります。

export default function CreateScreen({ onCreate, onBack }) {

次に、前回作ったhandleSubmitの最後で onCreateを呼び出します。

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

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

  onCreate(newItem);
};

ここで使っているonCreateは、 Appからpropsで渡す関数です。

CreateScreenonCreateを呼び出し、 引数としてnewItemを渡すことで、 親のAppからpropsで渡された関数を CreateScreenで呼び出し、作ったデータをAppへ渡します。

AppでnewItemを受け取る

もう一度、src/App.jsxを開きます。

次に、App側で CreateScreenへ渡す関数を作ります。

関数名はhandleCreateとします。

const handleCreate = (newItem) => {
  console.log("Appコンポーネントです");
  console.log(newItem);
};

そして、新規登録画面を表示する部分で、 handleCreateonCreateというpropsとして渡します。

if (screen === "create") {
  return (
    <CreateScreen
      onCreate={handleCreate}
      onBack={() => setScreen("list")}
    />
  );
}

これで、次の流れができました。

CreateScreen
onCreate(newItem)
  ↓
App
handleCreate(newItem)

フォームに値を入力して「登録する」を押し、 Consoleに「Appコンポーネントです」とnewItemが表示されれば、 CreateScreenからAppへ データを渡せています。

確認できたら、console.log()はこのあと一覧追加の処理へ置き換えます。

追加するデータにIDを付ける

引き続き、src/App.jsxを編集します。

先ほど作ったhandleCreateconsole.log()を削除し、 受け取ったnewItemをコピーして、 idを追加したitemWithIdを作る処理へ置き換えます。

これまでのサンプルデータには、 sample-1のようにあらかじめIDを付けていました。 今回は新しいデータを登録するため、IDも新しく作る必要があります。

今回は、crypto.randomUUID()を使ってIDを作ります。

const handleCreate = (newItem) => {
  // 学習用アプリのため、ブラウザ側でIDを生成
  const itemWithId = {
    ...newItem,
    id: crypto.randomUUID(),
  };
};

crypto.randomUUID()を実行すると、 UUIDと呼ばれるランダムな文字列が作られます。 今回は、それぞれのデータを区別するIDとして使います。

newItemをコピーして、IDを追加した itemWithIdができました。

itemsへ新しいデータを追加する

引き続き、src/App.jsxを編集します。

ID付きのデータを作れたので、setItemsを使って一覧データを更新します。

setItems((prevItems) => [
  ...prevItems,
  itemWithId,
]);

prevItemsには、 更新前のitemsが入ります。

...prevItemsで現在の一覧データをコピーし、最後にitemWithIdを追加した新しい配列を作っています。

最後に、handleCreateの末尾へ setScreen("list")を追加して、一覧画面へ戻します。

const handleCreate = (newItem) => {
  // 学習用アプリのため、ブラウザ側でIDを生成
  const itemWithId = {
    ...newItem,
    id: crypto.randomUUID(),
  };

  setItems((prevItems) => [
    ...prevItems,
    itemWithId,
  ]);

  setScreen("list");
};

これで、新規登録画面で入力したデータを 一覧へ追加できるようになりました。

ブラウザで確認する

ファイルを保存して、 実際に新しいデータを登録してみましょう。

「新規登録」を押してフォームを入力し、 「登録する」を押します。

一覧画面へ戻り、 入力したデータが一覧の最後に追加されていればOKです。

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

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

今回登場したReactとJavaScriptの要素
種類 今回登場したもの
React State、更新関数、props、親からpropsで渡された関数を子コンポーネントが呼び出して親へデータを知らせる処理
JavaScript crypto.randomUUID()、スプレッド構文、配列・オブジェクト

一覧データをStateとして管理し、 更新関数で新しい配列へ置き換える部分はReactでよく使う考え方です。

一方、IDを作ったり、 配列やオブジェクトをコピーしたりする部分はJavaScriptです。

今回のまとめ

  • itemsをStateで管理できるようにした
  • CreateScreenからonCreatenewItemを渡した
  • ApphandleCreatenewItemを受け取った
  • crypto.randomUUID()で新しいIDを作った
  • newItemをコピーして、IDを追加したitemWithIditemsへ追加した
  • Stateの配列を直接変更せず、新しい配列を作って更新した
  • 登録後に一覧画面へ戻した

今回の完成コード

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

src/App.jsx

import { useState } from "react";

import CreateScreen from "./components/CreateScreen";
import EditScreen from "./components/EditScreen";
import ListScreen from "./components/ListScreen";
import { initialItems } from "./initialItems";

export default function App() {
  const [items, setItems] = useState(initialItems);
  const [screen, setScreen] = useState("list");

  const handleCreate = (newItem) => {
    // 学習用アプリのため、ブラウザ側でIDを生成
    const itemWithId = {
      ...newItem,
      id: crypto.randomUUID(),
    };

    setItems((prevItems) => [
      ...prevItems,
      itemWithId,
    ]);

    setScreen("list");
  };

  if (screen === "create") {
    return (
      <CreateScreen
        onCreate={handleCreate}
        onBack={() => setScreen("list")}
      />
    );
  }

  if (screen === "edit") {
    return <EditScreen onBack={() => setScreen("list")} />;
  }

  return (
    <ListScreen
      items={items}
      onCreate={() => setScreen("create")}
      onEdit={() => setScreen("edit")}
    />
  );
}

src/components/CreateScreen.jsx

import { useState } from "react";

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

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

export default function CreateScreen({ onCreate, 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,
    };

    onCreate(newItem);
  };

  return (
    <main className="form-screen">
      <header className="screen-header">
        <h1>気になるものリスト</h1>
        <button
          type="button"
          className="secondary"
          onClick={onBack}
        >
          一覧へ
        </button>
      </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で保存して完成
スポンサーリンク