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

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

今回やること

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

また、フォームの入力値を inputValues Stateで管理し、 入力内容を変更できるところまで進めています。

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

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

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

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

スポンサーリンク

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

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

前回作った編集フォームには、まだ更新するためのボタンがありません。

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

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

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

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

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

src/components/EditScreen.jsxformを次のように変更してください。

<form onSubmit={handleSubmit}>

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

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

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

event.preventDefault()は、 第8回の新規登録フォームでも使いました。

今回もフォーム本来の送信処理ではなく、 React側のJavaScriptでデータを更新するため、 フォームの標準動作を止めています。

更新後のデータを作る

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

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

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

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

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

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

inputValuesには、 編集フォームの現在の入力値が入っています。

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

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

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

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

名前などを変更
↓

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

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

更新後のデータをAppコンポーネントへ渡す

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

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

今回は、その関数を 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コンポーネントへ渡します。

EditScreenコンポーネント
updatedItemを作る
↓
onSave(updatedItem)
↓
Appコンポーネント

この流れは、第9回で CreateScreenコンポーネントから onCreate(newItem)を呼び出したときと同じです。

Appコンポーネントで更新後のデータを受け取る

次に、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)が実行されます。

EditScreenコンポーネント
onSave(updatedItem)
↓
Appコンポーネント
handleUpdate(updatedItem)

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

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

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

更新するデータだけを置き換える

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

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

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

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

先ほど作ったsrc/App.jsxhandleUpdateを、次のように変更してください。

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

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

そして、 prevItems.map()で 一覧データを1件ずつ確認します。

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

現在確認しているitem.idと、 更新後のデータのupdatedItem.idが一致する場合は、 updatedItemを返します。

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

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

一致する
↓
updatedItemを返す

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

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

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

新しい配列でitems Stateを更新する

map()は、 元の配列の要素を1件ずつ処理し、 その結果から新しい配列を作ります。

今回は、 更新するデータだけupdatedItemを返し、 それ以外は元のitemを返しています。

そのため、 元のitems配列を直接変更せずに、 更新後の新しい配列を作れます。

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

更新後に一覧画面へ戻る

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

また、編集が終わったので、 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、フォーム送信、親からpropsで渡された関数を子コンポーネントが呼び出して親へ更新データを知らせる処理
JavaScript map()、スプレッド構文、trim()Number()

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

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

今回のまとめ

  • 編集フォームに「更新する」ボタンを追加した
  • inputValuesからupdatedItemを作った
  • 編集前と同じidを更新後のデータにも使用した
  • EditScreenコンポーネントからonSaveで更新後のデータを渡した
  • AppコンポーネントのhandleUpdateで更新後のデータを受け取った
  • map()で対象のデータだけをupdatedItemへ置き換えた
  • 第9回の追加処理と同じように、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で保存して完成
スポンサーリンク