今回やること
前回は、新規登録画面で作ったデータをAppコンポーネントへ渡し、一覧へ追加できるようにしました。
今回は、一覧の「編集」ボタンをクリックしたときに、
どのデータを編集するのかを覚えておき、
そのデータを編集画面のフォームへ表示します。
編集画面では、新規登録画面と同じように
入力途中の値をStateで管理します。
一覧の「編集」をクリック
↓
編集するデータのIDをAppコンポーネントで覚える
↓
itemsから対象のデータを探す
↓
EditScreenコンポーネントへ渡す
↓
フォームに現在の値を表示する
今回は、編集するデータをフォームへ表示するところまで進めます。
実際にデータを更新する処理は次回作ります。
編集するデータを覚えておく
一覧には複数のデータがあるため、編集画面へ移動するときにどのデータを編集するのかを覚えておく必要があります。
今回は、編集するデータのidをStateとして管理します。
src/App.jsxを開き、screen Stateの下に、次のStateを追加してください。
const [editingId, setEditingId] = useState(null);
editingIdが現在編集するデータのIDを保持するstate変数、
setEditingIdがその値を更新するための更新関数です。
最初はまだ編集するデータが決まっていないため、初期値にはnullを指定しています。
editingId
↓
null
編集するデータを選ぶ
↓
"sample-1" などのID
編集するデータをitemsから探す
次に、editingIdと一致するデータをitemsから探します。
編集するファイルは、引き続きsrc/App.jsxです。
AppコンポーネントのStateの下に、次のコードを追加してください。
const editingItem = items.find((item) => item.id === editingId) ?? null;
find()は、
条件に一致する最初の要素を配列から探すJavaScriptのメソッドです。
今回は、itemsを1件ずつ確認し、item.idとeditingIdが一致するデータを探しています。
たとえば、editingIdが
"sample-2"なら、
idが"sample-2"のデータが
editingItemに入ります。
条件に一致するデータが見つからない場合、
find()はundefinedを返します。
今回は?? nullを付けて、
見つからなかった場合はnullにしています。
編集ボタンからIDをAppコンポーネントへ渡す
次に、一覧の「編集」ボタンをクリックしたときに、
編集するデータのIDをAppコンポーネントへ渡せるようにします。
引き続き、src/App.jsxを編集します。
AppコンポーネントにstartEditを追加してください。
const startEdit = (id) => {
setEditingId(id);
setScreen("edit");
};
setEditingId(id)で編集するデータのIDを覚え、
setScreen("edit")で編集画面へ切り替えています。
次に、ListScreenコンポーネントへ
startEditをonEditというpropsで渡します。
return <ListScreen
items={items}
onCreate={() => setScreen("create")}
onEdit={startEdit}
/>
編集ボタンを一覧の行へ移す
次に、src/components/ListScreen.jsxを開きます。
これまでは、動作確認のため
「編集」ボタンを一覧画面のheaderに置いていました。
しかし、実際に編集するには、
どのデータを編集するのか選ぶ必要があります。
そこで、「編集」ボタンをheaderから削除し、
一覧の各データの行へ移します。
headerには「新規登録」ボタンだけを残します。
<header className="list-header">
<h1>気になるものリスト</h1>
<div className="header-actions">
<button type="button" onClick={onCreate}>
新規登録
</button>
</div>
</header>
次に、一覧を表示しているmap()の中で、
各行に「編集」ボタンを追加します。
map()で作っている各行の最後に、
「操作」列のtdを追加します。
{items.map((item) => (
<tr key={item.id}>
<td>{item.name}</td>
<td>{item.category}</td>
<td>{item.status}</td>
<td>{item.price.toLocaleString()}円</td>
<td>{item.memo}</td>
<td>
<button
type="button"
onClick={() => onEdit(item.id)}
>
編集
</button>
</td>
</tr>
))}
「編集」をクリックすると、
onEdit(item.id)によって
そのデータのIDがAppコンポーネントのstartEditへ渡されます。
編集するデータをEditScreenコンポーネントへ渡す
Appコンポーネントでは、
editingIdを使ってeditingItemを取得できるようになりました。
今度は、そのデータを
EditScreenコンポーネントへpropsで渡します。
src/App.jsxの編集画面を表示している部分を、
次のように変更してください。
if (screen === "edit" && editingItem) {
return (
<EditScreen
item={editingItem}
onBack={() => setScreen("list")}
/>
);
}
item={editingItem}によって、
編集する1件分のデータをEditScreenコンポーネントへ渡しています。
また、
screen === "edit" && editingItemとしているため、
編集画面で、なおかつ編集するデータが取得できている場合にだけ
EditScreenコンポーネントを表示します。
ここで一度、選んだデータを正しく取得できているか確認してみましょう。
if文の中で、
returnの前にconsole.log(editingItem)を追加します。
if (screen === "edit" && editingItem) {
console.log(editingItem);
return (
<EditScreen
item={editingItem}
onBack={() => setScreen("list")}
/>
);
}
ファイルを保存してブラウザを確認し、
一覧の「編集」ボタンをクリックしてください。
選択したデータがコンソールに表示されれば、編集するデータをAppコンポーネントで正しく取得できています。
選択したデータがコンソールに表示されます。
確認できたら、
console.log(editingItem)は削除しておきましょう。
編集フォームの初期値をStateにする
次に、src/components/EditScreen.jsxを開きます。
まず、useStateと、
カテゴリ・ステータスの選択肢を読み込みます。
import { useState } from "react";
import { categories, statuses } from "../options";
EditScreenコンポーネントでは、
propsで受け取ったitemの値を使って、
編集フォームのStateを用意します。
export default function EditScreen({ item, onBack }) {
const [inputValues, setInputValues] = useState({
name: item.name,
category: item.category,
status: item.status,
price: String(item.price),
memo: item.memo,
});
// このあと編集フォームを作ります
}
新規登録画面では空の入力値から始めましたが、
編集画面では現在のデータを初期値として使います。
価格はinputで扱いやすいように、
String()で文字列へ変換しておきます。
入力値を変更できるようにする
引き続き、src/components/EditScreen.jsxを編集します。
編集フォームでも、
第8回の新規登録フォームと同じように
handleChangeを使います。
const handleChange = (event) => {
const fieldName = event.target.name;
const value = event.target.value;
const newInputValues = { ...inputValues };
newInputValues[fieldName] = value;
setInputValues(newInputValues);
};
name属性で変更された項目を判断し、
現在のinputValuesをコピーして、
その項目だけを書き換えています。
この部分は新規登録画面と同じ考え方です。
編集フォームを表示する
最後に、inputValuesを使って編集フォームを表示します。
各入力欄のvalueには、inputValuesの対応する値を指定します。
たとえば名前の入力欄では、value={inputValues.name}としています。
inputValuesの初期値には編集するデータの現在の値を入れているため、
その値がvalueを通してフォームに表示されます。
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>
<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>
</form>
</main>
);
ファイルを保存して、一覧の各データにある「編集」ボタンを押してみましょう。
選んだデータの名前・カテゴリ・ステータス・価格・メモが
編集画面のフォームに表示されればOKです。
選択したデータの現在の値が編集フォームに表示されています。
この時点では、入力欄の内容を変更しても
一覧データそのものはまだ更新されません。
実際に更新する処理は次回追加します。
ReactとJavaScriptを分けて見てみよう
今回登場した内容を、
ReactとJavaScriptに分けて整理します。
今回登場したReactとJavaScriptの要素
| 種類 |
今回登場したもの |
| React |
State、props、画面切り替え、フォームの入力値の管理 |
| JavaScript |
find()、String()、オブジェクト、スプレッド構文 |
今回のまとめ
- 編集するデータのIDを
editingId Stateで管理した
find()でitemsから編集するデータを取得した
- 一覧の各行から編集するデータのIDを
Appコンポーネントへ渡した
- 編集するデータを
EditScreenへpropsで渡した
- 現在のデータを編集フォームのStateの初期値にした
- 新規登録画面と同じ
handleChangeで入力値を変更できるようにした
今回の完成コード
今回変更した
App.jsx、ListScreen.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");
};
if (screen === "create") {
return (
<CreateScreen
onCreate={handleCreate}
onBack={() => setScreen("list")}
/>
);
}
if (screen === "edit" && editingItem) {
return (
<EditScreen
item={editingItem}
onBack={() => setScreen("list")}
/>
);
}
return (
<ListScreen
items={items}
onCreate={() => setScreen("create")}
onEdit={startEdit}
/>
);
}
src/components/ListScreen.jsx
export default function ListScreen({ items, onCreate, onEdit }) {
return (
<main className="list-screen">
<header className="list-header">
<h1>気になるものリスト</h1>
<div className="header-actions">
<button type="button" onClick={onCreate}>
新規登録
</button>
</div>
</header>
<h2 className="screen-title">一覧画面</h2>
<table>
<thead>
<tr>
<th>名前</th>
<th>カテゴリ</th>
<th>ステータス</th>
<th>価格</th>
<th>メモ</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.id}>
<td>{item.name}</td>
<td>{item.category}</td>
<td>{item.status}</td>
<td>{item.price.toLocaleString()}円</td>
<td>{item.memo}</td>
<td>
<button
type="button"
onClick={() => onEdit(item.id)}
>
編集
</button>
</td>
</tr>
))}
</tbody>
</table>
</main>
);
}
src/components/EditScreen.jsx
import { useState } from "react";
import { categories, statuses } from "../options";
export default function EditScreen({ item, 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);
};
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>
<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>
</form>
</main>
);
}