編集画面を作ろう

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

今回やること

前回は、新規登録画面で作ったデータを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.ideditingIdが一致するデータを探しています。

たとえば、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コンポーネントへ startEditonEditという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.jsxListScreen.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");
  };

  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>
  );
}
スポンサーリンク