データを削除しよう

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

今回やること

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

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

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

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

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

スポンサーリンク

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

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

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

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

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

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

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

  <button type="button" className="danger">
    削除する
  </button>
</div>

「更新する」はフォームを送信するため type="submit"のままです。

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

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

削除するデータのIDをAppコンポーネントへ渡す

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

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

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

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

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

次に、「削除する」ボタンをクリックしたときに onDeleteを呼び出します。

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

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

そのため、 onDelete(item.id)を実行すると、 削除するデータのIDを Appコンポーネントへ渡せます。

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")}
    />
  );
}

これで、編集画面の「削除する」をクリックすると、 Appコンポーネントの handleDelete(id)が実行されます。

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

Consoleに選択したデータのIDが表示されれば、 親からpropsで渡された関数をEditScreenコンポーネントで呼び出し、 Appコンポーネントへ正しくIDを渡せています。

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

削除する前に確認する

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

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

先ほど作ったAppコンポーネントの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の処理を終了します。

そのため、「キャンセル」を選んだ場合は items Stateを変更しません。

削除するデータを除いた配列を作る

削除してよいことを確認できたら、 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件だけを除いた新しい配列 が作られます。

削除後に一覧画面へ戻る

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

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

handleDeleteを、 次の形にしてください。

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」を選んでみましょう。

一覧画面へ戻り、 選んだデータだけが一覧からなくなっていればOKです。

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

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

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

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

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

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

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

今回登場したReactとJavaScriptの要素
種類 今回登場したもの
React State、更新関数、props、親からpropsで渡された関数を子コンポーネントが呼び出して親へIDを知らせる処理
JavaScript filter()window.confirm()、条件分岐

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

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

今回のまとめ

  • 編集画面に「削除する」ボタンを追加した
  • EditScreenコンポーネントからonDeleteで削除するデータのIDを渡した
  • Appコンポーネントの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で保存して完成
スポンサーリンク