今回やること
前回は、一覧の「編集」ボタンから編集するデータを選び、
そのデータを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.jsxのformを次のように変更してください。
<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コンポーネントを表示している部分で、
handleUpdateをonSaveとして渡します。
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.jsxのhandleUpdateを、次のように変更してください。
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()は一覧を表示するときだけでなく、
配列の中の特定のデータを置き換えたいときにも使えます。
更新後に一覧画面へ戻る
最後に、
データを更新したあと一覧画面へ戻るようにします。
また、編集が終わったので、
editingIdもnullへ戻しておきます。
src/App.jsxのhandleUpdateを次の形にしてください。
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.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");
};
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で保存して完成