データを削除しよう

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

今回やること

前回は、編集フォームで変更したデータを EditScreenコンポーネントから Appコンポーネントへ渡し、 items Stateを更新できるようにしました。

今回は、編集画面に「削除する」ボタンを追加し、 選択しているデータを一覧から削除できるようにします。

編集画面の「削除する」をクリック
↓
削除するデータのIDをAppコンポーネントへ渡す
↓
削除してよいか確認する
↓
対象のデータを除いた新しい配列を作る
↓
items Stateを更新する
↓
一覧画面へ戻る

第9回の追加、第11回の更新と同じように、 今回も元のitems配列は直接変更しません。

削除するデータだけを除いた新しい配列を作り、その配列でStateを更新します。

スポンサーリンク

1. 削除するデータのIDをAppへ渡そう

まずは、編集画面に「削除する」ボタンを追加し、 削除するデータのIDをAppへ渡せるようにします。

「削除する」ボタンを追加する

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

現在の編集フォームには、 前回追加した「更新する」ボタンがあります。

今回は、その横に「削除する」ボタンを追加します。

更新と削除はどちらも編集フォームに対する操作なので、 第2回で用意したform-actionsを使って 1つのまとまりにします。

現在の「更新する」ボタンを、 次のコードに置き換えてください。

<div className="form-actions">
  <button type="submit">
    更新する
  </button>

  <button
    type="button"
    className="danger"
    onClick={() => onDelete(item.id)}
  >
    削除する
  </button>
</div>

「更新する」はフォームを送信するため、 type="submit"のままです。
一方、「削除する」はフォームを送信するボタンではないため、 type="button"を指定しています。

また、削除する操作だと分かりやすいように、 第2回で用意したdangerクラスを指定しています。

EditScreenからIDを渡す

items Stateを持っているのは EditScreenではなく、 Appコンポーネントです。

そのため、前回の更新処理と同じように、 親からpropsで渡された関数を EditScreenで呼び出して、 削除するデータのIDをAppへ渡します。

今回は、その関数を onDeleteというpropsで受け取ります。

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

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

「削除する」ボタンでは、 onDelete(item.id)を実行しています。

item.idには、 現在編集しているデータのIDが入っています。

EditScreenコンポーネント
現在編集中のitem.id
↓
onDelete(item.id)
↓
Appコンポーネント

AppでIDを受け取る

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

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

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

const handleDelete = (id) => {
  console.log(id);
};

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

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

削除するデータのIDをConsoleで確認しよう

ファイルを保存して一覧からデータを1件選び、 編集画面の「削除する」を押してみましょう。

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

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

2. 削除する前に確認しよう

「削除する」ボタンを押しただけですぐにデータが消えてしまうと、 誤操作で削除してしまう可能性があります。

そこで、削除する前に window.confirm()を使って確認します。

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

const handleDelete = (id) => {
  const shouldDelete = window.confirm("この項目を削除しますか?");

  if (!shouldDelete) {
    return;
  }

  // このあとデータを削除します
};

window.confirm()を実行すると、 ブラウザに確認ダイアログが表示されます。

「OK」を選ぶとtrue、 「キャンセル」を選ぶとfalseが返されます。
今回は、その結果をshouldDeleteに入れています。

「OK」を選ぶ
↓
shouldDelete → true
↓
削除処理へ進む

「キャンセル」を選ぶ
↓
shouldDelete → false
↓
returnで処理を終了する

if (!shouldDelete)に当てはまる場合は、 returnhandleDeleteの処理を終了します。

キャンセルすると削除されないことを確認しよう

ファイルを保存して、 編集画面の「削除する」をクリックしてください。

確認ダイアログで「キャンセル」を選び、 編集画面のまま何も変わらなければOKです。

この時点ではまだ削除処理を追加していないため、 「OK」を選んだ場合もデータは削除されません。
次に、実際の削除処理を作ります。

3. 削除するデータを除いた配列を作ろう

削除してよいことを確認できたら、 items Stateから対象のデータを削除します。

ただし、これまでと同じように、 元のitems配列を直接変更することはしません。

今回は、 削除するデータ以外を残した新しい配列 を作ります。

そのために使うのが、 JavaScriptのfilter()です。

handleDeleteへ、 次の処理を追加してください。

const handleDelete = (id) => {
  const shouldDelete = window.confirm("この項目を削除しますか?");

  if (!shouldDelete) {
    return;
  }

  setItems((prevItems) =>
    prevItems.filter((item) => item.id !== id)
  );
};

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

prevItems.filter()で一覧データを1件ずつ確認し、 item.idと削除するid一致しないデータだけを残します。

item.id !== id
prevItemsをfilter()で1件ずつ確認
↓
item.id と削除するidが違う?

違う
↓
残す

同じ
↓
残さない

↓
削除対象を除いた新しい配列ができる

たとえば、 sample-2を削除する場合は、 次のようになります。

sample-1
→ 削除するIDと違う
→ 残す

sample-2
→ 削除するIDと一致
→ 残さない

その結果、 削除する1件だけを除いた新しい配列 が作られます。

filter()を使うと、 条件に合うデータだけを残した新しい配列を作れます。

4. 削除後に一覧画面へ戻ろう

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

削除したデータはもう編集しないため、 editingIdnullへ戻します。

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

const handleDelete = (id) => {
  const shouldDelete = window.confirm("この項目を削除しますか?");

  if (!shouldDelete) {
    return;
  }

  setItems((prevItems) =>
    prevItems.filter((item) => item.id !== id)
  );

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

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

選んだデータが削除されることを確認しよう

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

一覧から削除したいデータの「編集」を押し、 編集画面の「削除する」をクリックします。

確認ダイアログで「OK」を選び、 一覧画面へ戻ったあと、 選んだデータだけが一覧からなくなっていればOKです。

追加・更新・削除を振り返る

ここまでで、 一覧データの追加・更新・削除ができるようになりました。

3つの処理では使っているJavaScriptの書き方は異なりますが、 ReactのStateを更新するときの考え方は共通しています。

一覧データを変更するときの処理
操作 使用した方法 作る配列
追加 スプレッド構文 新しいデータを追加した配列
更新 map() 対象のデータを置き換えた配列
削除 filter() 対象のデータを除いた配列

どの処理でも、 元のStateを直接変更せず、新しい配列を作って更新関数へ渡しています。

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

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

今回登場したReactとJavaScriptの要素
種類 今回登場したもの
React State、更新関数、props、子コンポーネントから親コンポーネントへ削除するIDを渡す処理
JavaScript filter()window.confirm()、条件分岐

filter()で削除対象を除いた新しい配列を作る部分や、 window.confirm()で確認する部分はJavaScriptです。

その新しい配列を更新関数setItemsへ渡し、 items Stateを更新することで、 Reactの一覧画面から対象のデータがなくなります。

今回のまとめ

  • 編集画面に「削除する」ボタンを追加した
  • onDeleteを使ってEditScreenからAppへ削除するデータのIDを渡した
  • handleDeleteで削除するIDを受け取った
  • window.confirm()で削除前に確認できるようにした
  • filter()で削除対象を除いた新しい配列を作った
  • Stateの配列を直接変更せず、新しい配列でitems Stateを更新した
  • 削除後にeditingIdをリセットして一覧画面へ戻した
  • 追加・更新・削除では、どれも新しい配列を作ってStateを更新している

今回の完成コード

今回変更した 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");
  };

  const handleDelete = (id) => {
    const shouldDelete = window.confirm("この項目を削除しますか?");

    if (!shouldDelete) {
      return;
    }

    setItems((prevItems) =>
      prevItems.filter((item) => item.id !== id)
    );

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

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

  if (screen === "edit" && editingItem) {
    return (
      <EditScreen
        item={editingItem}
        onSave={handleUpdate}
        onDelete={handleDelete}
        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, onDelete, 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>

        <div className="form-actions">
          <button type="submit">
            更新する
          </button>

          <button
            type="button"
            className="danger"
            onClick={() => onDelete(item.id)}
          >
            削除する
          </button>
        </div>
      </form>
    </main>
  );
}

アプリの完成状況

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