今回やること
前回は、一覧から編集するデータを選び、
そのデータをEditScreenコンポーネントのフォームへ表示しました。
今回は、編集フォームに入力した内容から更新後のデータを作り、
Appコンポーネントへ渡して、
実際のitems Stateを更新します。
編集フォームの「更新する」をクリック
↓
inputValuesから更新後のデータを作る
↓
EditScreenコンポーネントからAppコンポーネントへ渡す
↓
更新するデータだけを置き換えた新しい配列を作る
↓
items Stateを更新する
↓
一覧画面へ戻る
第9回でデータを追加したときも、
元のitems配列を直接変更せず、
新しい配列を作ってsetItemsへ渡しました。
今回の更新でも考え方は同じです。
元のStateを直接変更せず、更新後の新しい配列を作ってStateを更新します。
1. 編集フォームから更新後のデータを作ろう
まずは、編集フォームから
更新後の1件分のデータを作れるようにします。
「更新する」ボタンを追加する
src/components/EditScreen.jsxを開きます。
formの最後、
メモの入力欄の下に「更新する」ボタンを追加してください。
<button type="submit">
更新する
</button>
新規登録画面の「登録する」ボタンと同じように、
type="submit"を指定しています。
フォームの送信処理を作る
編集フォームの送信処理も、
第8回の新規登録フォームと同じように
onSubmitで受け取ります。
formの開始タグを次のように変更してください。
<form onSubmit={handleSubmit}>
続いて、EditScreenコンポーネントの中で、
handleChangeの下に
handleSubmitを追加します。
const handleSubmit = (event) => {
event.preventDefault();
// このあと更新後のデータを作ります
};
event.preventDefault()は、
第8回の新規登録フォームでも使いました。
今回もフォーム本来の送信処理ではなく、
React側のJavaScriptでデータを更新するため、
フォームの標準動作を止めています。
更新後のデータを作る
次に、編集フォームの現在の入力内容から
更新後のデータを作ります。
先ほど追加したhandleSubmitを、
次のように変更してください。
const handleSubmit = (event) => {
event.preventDefault();
const updatedItem = {
...inputValues,
id: item.id,
name: inputValues.name.trim(),
price: Number(inputValues.price) || 0,
};
// このあとAppコンポーネントへ渡します
};
...inputValuesでフォームの入力内容をコピーし、
更新後のデータをupdatedItemとして作っています。
名前は第8回の新規登録と同じように
trim()で前後の空白を取り除き、
価格はNumber()で数値に変換しています。
また、編集してもデータそのものは同じ1件なので、
idには新しい値を作らず、
もとのitem.idをそのまま使います。
編集前
{
id: "sample-1",
name: "はじめてのWebデザイン本",
...
}
名前などを変更
↓
編集後
{
id: "sample-1",
name: "Reactで学ぶWebデザイン",
...
}
このidを使って、
あとでitemsの中から
更新するデータを判断します。
2. 更新後のデータをAppへ渡そう
updatedItemを作れましたが、
items Stateを持っているのは
EditScreenではなく
Appコンポーネントです。
第9回の新規登録と同じように、
親からpropsで渡された関数を子コンポーネントで呼び出し、
更新後のデータをAppへ渡します。
EditScreenからupdatedItemを渡す
今回は、更新後のデータを渡す関数を
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へ渡します。
AppでupdatedItemを受け取る
次に、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)
更新後のデータをConsoleで確認しよう
ファイルを保存して、
一覧からデータを1件選んで編集し、
「更新する」を押してみましょう。
Consoleに更新後のデータが表示されれば、
EditScreenから
Appへ正しくデータを渡せています。
確認できたら、
console.log(updatedItem)は
次の更新処理へ置き換えます。
3. 更新するデータだけを置き換えよう
更新後のデータを
Appで受け取れるようになりました。
次は、items Stateの中から
同じidを持つデータだけを
updatedItemへ置き換えます。
第10回では、
編集する1件を取得するためにfind()を使いました。
今回は1件を取得するのではなく、
更新対象だけを置き換えた新しい配列を作りたいので、
map()を使います。
先ほど追加した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.id === updatedItem.id ? updatedItem : item
一致しない場合は、
もとのitemをそのまま返します。
prevItemsをmap()で1件ずつ確認
↓
item.id と updatedItem.id が一致する?
一致する
↓
updatedItemを返す
一致しない
↓
元のitemを返す
↓
更新対象だけが置き換わった新しい配列ができる
その結果、
編集した1件だけが更新後のデータへ置き換わります。
map()は一覧を表示するときだけでなく、
配列の中の特定のデータを置き換えたいときにも使えます。
4. 更新後に一覧画面へ戻ろう
最後に、
データを更新したあと一覧画面へ戻るようにします。
また、編集が終わったので、
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、フォーム送信、子コンポーネントから親コンポーネントへ更新後のデータを渡す処理 |
| JavaScript |
map()、スプレッド構文、trim()、Number() |
updatedItemを作ったり、
map()で新しい配列を作ったりする部分は
JavaScriptです。
その新しい配列を更新関数setItemsへ渡し、
items Stateを更新することで、
Reactの画面にも新しい内容が反映されます。
今回のまとめ
- 編集フォームの入力値から
updatedItemを作った
- 更新後のデータでも編集前と同じ
idを使用した
onSaveを使ってEditScreenからAppへ更新後のデータを渡した
handleUpdateで更新後のデータを受け取った
map()で対象のデータだけをupdatedItemへ置き換えた
- 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で保存して完成