今回やること
前回は、編集フォームで変更したデータを
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コンポーネントを表示している部分で、
handleDeleteを
onDeleteとして渡します。
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)に当てはまる場合は、
returnでhandleDeleteの処理を終了します。
そのため、「キャンセル」を選んだ場合は
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件だけを除いた新しい配列
が作られます。
削除後に一覧画面へ戻る
最後に、
データを削除したあと一覧画面へ戻るようにします。
削除したデータはもう編集しないため、
editingIdもnullへ戻します。
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.jsxと
EditScreen.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で保存して完成