編集したデータで更新しよう

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

今回やること

前回は、一覧から編集するデータを選び、 そのデータをEditScreenコンポーネントのフォームへ表示しました。

今回は、編集フォームに入力した内容から更新後のデータを作り、 Appコンポーネントへ渡して、 実際のitems Stateを更新します。

編集フォームの「更新する」をクリック
↓
inputValuesから更新後のデータを作る
↓
EditScreenコンポーネントからAppコンポーネントへ渡す
↓
更新するデータだけを置き換えた新しい配列を作る
↓
items Stateを更新する
↓
一覧画面へ戻る

第9回でデータを追加したときも、 元のitems配列を直接変更せず、 新しい配列を作ってsetItemsへ渡しました。

今回の更新でも考え方は同じです。 元のStateを直接変更せず、更新後の新しい配列を作ってStateを更新します。

スポンサーリンク

1. 編集フォームから更新後のデータを作ろう

まずは、編集フォームから 更新後の1件分のデータを作れるようにします。

「更新する」ボタンを追加する

src/components/EditScreen.jsxを開きます。

formの最後、 メモの入力欄の下に「更新する」ボタンを追加してください。

<button type="submit">
  更新する
</button>

新規登録画面の「登録する」ボタンと同じように、 type="submit"を指定しています。

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

編集フォームの送信処理も、 第8回の新規登録フォームと同じように onSubmitで受け取ります。

formの開始タグを次のように変更してください。

<form onSubmit={handleSubmit}>

続いて、EditScreenコンポーネントの中で、 handleChangeの下に handleSubmitを追加します。

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

  // このあと更新後のデータを作ります
};

event.preventDefault()は、 第8回の新規登録フォームでも使いました。
今回もフォーム本来の送信処理ではなく、 React側のJavaScriptでデータを更新するため、 フォームの標準動作を止めています。

更新後のデータを作る

次に、編集フォームの現在の入力内容から 更新後のデータを作ります。

先ほど追加したhandleSubmitを、 次のように変更してください。

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

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

  // このあとAppコンポーネントへ渡します
};

...inputValuesでフォームの入力内容をコピーし、 更新後のデータをupdatedItemとして作っています。

名前は第8回の新規登録と同じように trim()で前後の空白を取り除き、 価格はNumber()で数値に変換しています。

また、編集してもデータそのものは同じ1件なので、 idには新しい値を作らず、 もとのitem.idをそのまま使います。

編集前
{
  id: "sample-1",
  name: "はじめてのWebデザイン本",
  ...
}

名前などを変更
↓

編集後
{
  id: "sample-1",
  name: "Reactで学ぶWebデザイン",
  ...
}

このidを使って、 あとでitemsの中から 更新するデータを判断します。

2. 更新後のデータをAppへ渡そう

updatedItemを作れましたが、 items Stateを持っているのは EditScreenではなく Appコンポーネントです。

第9回の新規登録と同じように、 親からpropsで渡された関数を子コンポーネントで呼び出し、 更新後のデータをAppへ渡します。

EditScreenからupdatedItemを渡す

今回は、更新後のデータを渡す関数を onSaveというpropsで受け取ります。

EditScreenコンポーネントの引数を、 次のように変更してください。

export default function EditScreen({ item, onSave, onBack }) {

続いて、 handleSubmitの最後で onSaveを呼び出します。

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

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

  onSave(updatedItem);
};

onSave(updatedItem)によって、 更新後のデータをAppへ渡します。

AppでupdatedItemを受け取る

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

EditScreenから渡された updatedItemを受け取る handleUpdateを作ります。

startEditの下に、 次の関数を追加してください。

const handleUpdate = (updatedItem) => {
  console.log(updatedItem);
};

続いて、 EditScreenを表示している部分で、 handleUpdateonSaveとして渡します。

if (screen === "edit" && editingItem) {
  return (
    <EditScreen
      item={editingItem}
      onSave={handleUpdate}
      onBack={() => setScreen("list")}
    />
  );
}
EditScreenコンポーネント
onSave(updatedItem)
↓
Appコンポーネント
handleUpdate(updatedItem)

更新後のデータをConsoleで確認しよう

ファイルを保存して、 一覧からデータを1件選んで編集し、 「更新する」を押してみましょう。

Consoleに更新後のデータが表示されれば、 EditScreenから Appへ正しくデータを渡せています。

確認できたら、 console.log(updatedItem)は 次の更新処理へ置き換えます。

3. 更新するデータだけを置き換えよう

更新後のデータを Appで受け取れるようになりました。

次は、items Stateの中から 同じidを持つデータだけを updatedItemへ置き換えます。

第10回では、 編集する1件を取得するためにfind()を使いました。

今回は1件を取得するのではなく、 更新対象だけを置き換えた新しい配列を作りたいので、 map()を使います。

先ほど追加したhandleUpdateを、 次のように変更してください。

const handleUpdate = (updatedItem) => {
  setItems((prevItems) =>
    prevItems.map((item) =>
      item.id === updatedItem.id ? updatedItem : item
    )
  );
};

setItemsの中では、 更新前の一覧データをprevItemsとして受け取っています。

prevItems.map()で一覧データを1件ずつ確認し、 item.idupdatedItem.idが一致する場合だけ updatedItemを返します。

item.id === updatedItem.id ? updatedItem : item

一致しない場合は、 もとのitemをそのまま返します。

prevItemsをmap()で1件ずつ確認
↓
item.id と updatedItem.id が一致する?

一致する
↓
updatedItemを返す

一致しない
↓
元のitemを返す

↓
更新対象だけが置き換わった新しい配列ができる

その結果、 編集した1件だけが更新後のデータへ置き換わります。

map()は一覧を表示するときだけでなく、 配列の中の特定のデータを置き換えたいときにも使えます。

4. 更新後に一覧画面へ戻ろう

最後に、 データを更新したあと一覧画面へ戻るようにします。

また、編集が終わったので、 editingIdnullへ戻しておきます。

src/App.jsxhandleUpdateを次の形にしてください。

const handleUpdate = (updatedItem) => {
  setItems((prevItems) =>
    prevItems.map((item) =>
      item.id === updatedItem.id ? updatedItem : item
    )
  );

  setEditingId(null);
  setScreen("list");
};

setEditingId(null)で 編集中のデータをリセットし、 setScreen("list")で一覧画面へ戻しています。

編集したデータが更新されることを確認しよう

ファイルを保存して、 実際にデータを編集してみましょう。

一覧の「編集」を押して、 名前やカテゴリ、ステータス、価格、メモのいずれかを変更し、 「更新する」を押します。

一覧画面へ戻り、編集したデータだけが新しい内容に変わって、ほかのデータがそのまま残っていればOKです。

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

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

今回登場したReactとJavaScriptの要素
種類 今回登場したもの
React State、更新関数、props、フォーム送信、子コンポーネントから親コンポーネントへ更新後のデータを渡す処理
JavaScript map()、スプレッド構文、trim()Number()

updatedItemを作ったり、 map()で新しい配列を作ったりする部分は JavaScriptです。

その新しい配列を更新関数setItemsへ渡し、 items Stateを更新することで、 Reactの画面にも新しい内容が反映されます。

今回のまとめ

  • 編集フォームの入力値からupdatedItemを作った
  • 更新後のデータでも編集前と同じidを使用した
  • onSaveを使ってEditScreenからAppへ更新後のデータを渡した
  • handleUpdateで更新後のデータを受け取った
  • map()で対象のデータだけをupdatedItemへ置き換えた
  • Stateの配列を直接変更せず、更新後の新しい配列を作った
  • 更新後にeditingIdをリセットして一覧画面へ戻した

今回の完成コード

今回変更した App.jsxEditScreen.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 [editingId, setEditingId] = useState(null);

  const editingItem =
    items.find((item) => item.id === editingId) ?? null;

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

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

    setScreen("list");
  };

  const startEdit = (id) => {
    setEditingId(id);
    setScreen("edit");
  };

  const handleUpdate = (updatedItem) => {
    setItems((prevItems) =>
      prevItems.map((item) =>
        item.id === updatedItem.id ? updatedItem : item
      )
    );

    setEditingId(null);
    setScreen("list");
  };

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

  if (screen === "edit" && editingItem) {
    return (
      <EditScreen
        item={editingItem}
        onSave={handleUpdate}
        onBack={() => setScreen("list")}
      />
    );
  }

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

src/components/EditScreen.jsx

import { useState } from "react";

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

export default function EditScreen({ item, onSave, onBack }) {
  const [inputValues, setInputValues] = useState({
    name: item.name,
    category: item.category,
    status: item.status,
    price: String(item.price),
    memo: item.memo,
  });

  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 updatedItem = {
      ...inputValues,
      id: item.id,
      name: inputValues.name.trim(),
      price: Number(inputValues.price) || 0,
    };

    onSave(updatedItem);
  };

  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 edit-title">編集画面</h2>

      <form onSubmit={handleSubmit}>
        <label>
          名前
          <input
            name="name"
            value={inputValues.name}
            onChange={handleChange}
          />
        </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で保存して完成
スポンサーリンク