モダンJavaScriptの基礎から始める挫折しないためのReact入門 60日コース | フォーム・コンポーネント設計 - DAY 36:フォーム総合演習

React 60日で身につけるReact
スポンサーリンク
スポンサーリンク
  1. DAY 36:フォーム総合演習 前半 ― 簡易ユーザー登録アプリの全体像をつかむ
  2. アプリのゴールを言葉で整理する
    1. どんなことができるアプリなのか
  3. コンポーネント構成をシンプルに決める
    1. まずは1コンポーネントで完結させる
  4. ベースコンポーネントを作る(入力+一覧表示の枠)
    1. まずは「入力欄」と「一覧表示」の枠だけ作る
  5. 入力バリデーションを組み立てる
    1. 名前:必須+文字数チェック
    2. メール:必須+簡易形式チェック
  6. 登録処理を「バリデーションの後」に置く
    1. handleSubmitの中身をステップで整理する
  7. 一覧表示と削除機能をつなげる
    1. 削除ボタンを追加して、ユーザーを取り除く
  8. DAY 36 前半版テンプレート ― 簡易ユーザー登録アプリ
  9. DAY 36 前半のまとめ ― 「フォームの一連の流れ」を1つのアプリで体感する
  10. DAY 36:フォーム総合演習 中盤 ― 「少しだけ実戦寄り」に簡易ユーザー登録アプリを育てる
  11. アプリの「責務」を言葉で整理してみる
    1. 何を管理して、何を表示しているのか
  12. ユーザーオブジェクトの形をはっきり決める
    1. 「ユーザーとは何か」をコードで表現する
  13. バリデーションと登録処理を「読みやすく」する
    1. handleSubmitの中身を整理する
  14. 削除処理を「Stateの更新」として理解する
    1. filterで「残すものだけ」を選ぶ
  15. 一覧表示のUIを少しだけ整える
    1. 見やすさと操作しやすさを意識する
  16. DAY 36 中盤版テンプレート ― 少し整理された簡易ユーザー登録アプリ
  17. DAY 36 中盤のまとめ ― 「流れ+構造」を同時に意識できるようになる
  18. DAY 36:フォーム総合演習 後半 ― 「実戦を意識した簡易ユーザー登録アプリ」に仕上げる
  19. アプリ全体の流れをもう一度確認する
    1. 入力から削除までの一連のストーリー
  20. 入力とバリデーションを「ユーザー目線」で整える
    1. 入力欄にプレースホルダーとラベルを丁寧に付ける
  21. バリデーションを「実戦の入り口」として意識する
    1. 名前・メールのルールを明確にする
  22. 登録処理と一覧表示を「安全に・分かりやすく」つなぐ
    1. ユーザーオブジェクトの扱い方を整理する
  23. 削除処理を「State更新の基本パターン」として理解する
    1. filterで「残すものだけ」を選ぶ
  24. DAY 36 後半版テンプレート ― 実戦寄り簡易ユーザー登録アプリ
  25. DAY 36 後半のまとめ ― 「フォームの一連の流れ」を自分の手で完結させる

DAY 36:フォーム総合演習 前半 ― 簡易ユーザー登録アプリの全体像をつかむ

DAY 36では、これまで学んできた

  • 入力フォーム
  • Controlled Component
  • フォーム送信
  • バリデーション
  • 一覧表示
  • 削除

をひとつにまとめた「簡易ユーザー登録アプリ」を作っていきます。 前半では、まず全体像をつかみながら、入力〜バリデーション〜登録〜一覧表示〜削除までの流れを、シンプルな形で組み立てていきます。

アプリのゴールを言葉で整理する

どんなことができるアプリなのか

今回作る「簡易ユーザー登録アプリ」は、次のような動きをするものです。

  • ユーザーが「名前」と「メール」を入力する
  • 入力内容に対してバリデーションを行う
    • 名前:必須・最大20文字
    • メール:必須・「@」を含む簡易チェック
  • バリデーションを通過したら「登録」ボタンでユーザー一覧に追加される
  • 画面下部に「登録済みユーザー一覧」が表示される
  • 一覧の各ユーザーには「削除」ボタンがあり、クリックするとそのユーザーが一覧から消える

つまり、「フォーム+バリデーション+State管理+一覧表示+削除」という、Reactの基本要素が一通り詰まったミニアプリになります。

コンポーネント構成をシンプルに決める

まずは1コンポーネントで完結させる

前半では、構成をシンプルにするために、 1つのコンポーネントの中にすべてを書いてみる方針で進めます。

コンポーネント名は SimpleUserRegisterApp として、次のようなStateを持ちます。

  • 入力用State
    • name:名前入力欄
    • email:メール入力欄
  • バリデーション用State
    • nameError:名前のエラーメッセージ
    • emailError:メールのエラーメッセージ
  • 登録済みユーザー一覧
    • users[{ id, name, email }, ...] の配列

これをベースに、少しずつ機能を足していきます。

ベースコンポーネントを作る(入力+一覧表示の枠)

まずは「入力欄」と「一覧表示」の枠だけ作る

import { useState } from "react";

function SimpleUserRegisterApp() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  const [nameError, setNameError] = useState("");
  const [emailError, setEmailError] = useState("");

  const [users, setUsers] = useState([]);

  const handleSubmit = (e) => {
    e.preventDefault();
    // ここにバリデーション+登録処理を追加していきます
  };

  return (
    <div>
      <h2>DAY 36:フォーム総合演習前半 ― 簡易ユーザー登録アプリ</h2>

      {/* 入力フォーム */}
      <form onSubmit={handleSubmit}>
        <div>
          <label>
            名前:
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="名前を入力してください"
            />
          </label>
          {nameError && (
            <p style={{ color: "red", marginTop: "4px" }}>{nameError}</p>
          )}
        </div>

        <div style={{ marginTop: "8px" }}>
          <label>
            メール:
            <input
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              placeholder="メールアドレスを入力してください"
            />
          </label>
          {emailError && (
            <p style={{ color: "red", marginTop: "4px" }}>{emailError}</p>
          )}
        </div>

        <div style={{ marginTop: "12px" }}>
          <button type="submit">登録</button>
        </div>
      </form>

      {/* 一覧表示 */}
      <div style={{ marginTop: "24px" }}>
        <h3>登録済みユーザー一覧</h3>
        {users.length === 0 ? (
          <p>まだユーザーは登録されていません。</p>
        ) : (
          <ul>
            {users.map((user) => (
              <li key={user.id}>
                {user.name}{user.email}
                {/* 削除ボタンは後で追加します */}
              </li>
            ))}
          </ul>
        )}
      </div>
    </div>
  );
}

export default SimpleUserRegisterApp;
JSX

ここまでで、

  • 入力欄(名前・メール)
  • エラーメッセージ表示枠
  • 登録済みユーザー一覧の枠

ができました。 次に、バリデーションと登録処理を追加していきます。

入力バリデーションを組み立てる

名前:必須+文字数チェック

まず、名前のバリデーションを関数としてまとめます。

const MAX_NAME_LENGTH = 20;

const validateName = (name) => {
  if (name.trim() === "") {
    return "名前は必須です。";
  }

  if (name.length > MAX_NAME_LENGTH) {
    return `名前は${MAX_NAME_LENGTH}文字以内で入力してください。`;
  }

  return "";
};
JSX

メール:必須+簡易形式チェック

const validateEmail = (email) => {
  if (email.trim() === "") {
    return "メールアドレスは必須です。";
  }

  if (!email.includes("@")) {
    return "メールアドレスの形式が正しくありません。";
  }

  return "";
};
JSX

これらを handleSubmit の中で使います。

登録処理を「バリデーションの後」に置く

handleSubmitの中身をステップで整理する

handleSubmit の中で行うことを順番に整理すると、次のようになります。

  1. preventDefault() でブラウザのデフォルト送信を止める
  2. いったんエラーをクリアする
  3. 名前・メールのバリデーションを行う
  4. エラーがあればエラーメッセージをセットして処理を止める
  5. 問題なければユーザー一覧に追加する
  6. 入力欄をリセットする

これをコードにすると、次のようになります。

const handleSubmit = (e) => {
  e.preventDefault();

  setNameError("");
  setEmailError("");

  const nameValidationResult = validateName(name);
  const emailValidationResult = validateEmail(email);

  if (nameValidationResult !== "") {
    setNameError(nameValidationResult);
  }

  if (emailValidationResult !== "") {
    setEmailError(emailValidationResult);
  }

  if (nameValidationResult !== "" || emailValidationResult !== "") {
    // どちらかにエラーがあれば登録しない
    return;
  }

  // バリデーションを通過したので登録処理へ
  const newUser = {
    id: Date.now(), // 簡易的なID(実戦ではUUIDなどを使うことが多いです)
    name,
    email,
  };

  setUsers((prevUsers) => [...prevUsers, newUser]);

  // 入力欄をリセット
  setName("");
  setEmail("");
};
JSX

重要なポイント:

  • 「バリデーションに通過したら登録する」という流れを、handleSubmit の中で明確にしていること
  • setUsers で新しいユーザーを配列に追加していること
  • 登録後に入力欄を空にして、次の入力に備えていること

一覧表示と削除機能をつなげる

削除ボタンを追加して、ユーザーを取り除く

一覧表示部分に「削除」ボタンを追加し、クリックされたユーザーを一覧から取り除く処理を実装します。

まず、削除用の関数を作ります。

const handleDeleteUser = (id) => {
  setUsers((prevUsers) => prevUsers.filter((user) => user.id !== id));
};
JSX

一覧表示部分を次のように変更します。

<div style={{ marginTop: "24px" }}>
  <h3>登録済みユーザー一覧</h3>
  {users.length === 0 ? (
    <p>まだユーザーは登録されていません。</p>
  ) : (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          {user.name}{user.email}
          <button
            style={{ marginLeft: "8px" }}
            onClick={() => handleDeleteUser(user.id)}
          >
            削除
          </button>
        </li>
      ))}
    </ul>
  )}
</div>
JSX

これで、

  • 登録されたユーザーが一覧に表示される
  • 各ユーザーの横に「削除」ボタンが表示される
  • 削除ボタンを押すと、そのユーザーが一覧から消える

という流れが完成します。

DAY 36 前半版テンプレート ― 簡易ユーザー登録アプリ

ここまでの内容をすべて含んだ、 DAY 36前半の「簡易ユーザー登録アプリ」テンプレートをまとめます。

import { useState } from "react";

const MAX_NAME_LENGTH = 20;

function SimpleUserRegisterApp() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  const [nameError, setNameError] = useState("");
  const [emailError, setEmailError] = useState("");

  const [users, setUsers] = useState([]);

  const validateName = (name) => {
    if (name.trim() === "") {
      return "名前は必須です。";
    }

    if (name.length > MAX_NAME_LENGTH) {
      return `名前は${MAX_NAME_LENGTH}文字以内で入力してください。`;
    }

    return "";
  };

  const validateEmail = (email) => {
    if (email.trim() === "") {
      return "メールアドレスは必須です。";
    }

    if (!email.includes("@")) {
      return "メールアドレスの形式が正しくありません。";
    }

    return "";
  };

  const handleSubmit = (e) => {
    e.preventDefault();

    setNameError("");
    setEmailError("");

    const nameValidationResult = validateName(name);
    const emailValidationResult = validateEmail(email);

    if (nameValidationResult !== "") {
      setNameError(nameValidationResult);
    }

    if (emailValidationResult !== "") {
      setEmailError(emailValidationResult);
    }

    if (nameValidationResult !== "" || emailValidationResult !== "") {
      return;
    }

    const newUser = {
      id: Date.now(),
      name,
      email,
    };

    setUsers((prevUsers) => [...prevUsers, newUser]);

    setName("");
    setEmail("");
  };

  const handleDeleteUser = (id) => {
    setUsers((prevUsers) => prevUsers.filter((user) => user.id !== id));
  };

  return (
    <div>
      <h2>DAY 36:フォーム総合演習前半 ― 簡易ユーザー登録アプリ</h2>

      <form onSubmit={handleSubmit}>
        <div>
          <label>
            名前:
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="名前を入力してください"
            />
          </label>
          {nameError && (
            <p style={{ color: "red", marginTop: "4px" }}>{nameError}</p>
          )}
        </div>

        <div style={{ marginTop: "8px" }}>
          <label>
            メール:
            <input
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              placeholder="メールアドレスを入力してください"
            />
          </label>
          {emailError && (
            <p style={{ color: "red", marginTop: "4px" }}>{emailError}</p>
          )}
        </div>

        <div style={{ marginTop: "12px" }}>
          <button type="submit">登録</button>
        </div>
      </form>

      <div style={{ marginTop: "24px" }}>
        <h3>登録済みユーザー一覧</h3>
        {users.length === 0 ? (
          <p>まだユーザーは登録されていません。</p>
        ) : (
          <ul>
            {users.map((user) => (
              <li key={user.id}>
                {user.name}{user.email}
                <button
                  style={{ marginLeft: "8px" }}
                  onClick={() => handleDeleteUser(user.id)}
                >
                  削除
                </button>
              </li>
            ))}
          </ul>
        )}
      </div>
    </div>
  );
}

export default SimpleUserRegisterApp;
JSX

DAY 36 前半のまとめ ― 「フォームの一連の流れ」を1つのアプリで体感する

DAY 36前半で意識してほしいのは次のポイントです。

  • 入力 → バリデーション → 登録 → 一覧表示 → 削除 という一連の流れを、1つのコンポーネントの中でつなげられること。
  • バリデーションは、送信前に必ず通す「入口のフィルター」として機能していること。
  • 登録済みユーザー一覧は、State(配列)として管理し、追加・削除を配列操作で行うこと。

このミニアプリの流れが腑に落ちてくると、 この先の「より複雑なフォームアプリ」も、 同じパターンの拡張として見えてくるようになります。


DAY 36:フォーム総合演習 中盤 ― 「少しだけ実戦寄り」に簡易ユーザー登録アプリを育てる

DAY 36の中盤では、前半で作った

  • 入力
  • バリデーション
  • 登録
  • 一覧表示
  • 削除

を備えた「簡易ユーザー登録アプリ」を、もう一歩だけ実戦寄りの形に育てることを目指します。

ここでは特に、

  • コードの見通しをよくする
  • ユーザーオブジェクトの扱い方を整理する
  • 削除処理の意味をもう少し深く理解する

という観点で、ステップバイステップでかみ砕いて説明していきます。

アプリの「責務」を言葉で整理してみる

何を管理して、何を表示しているのか

前半で作った簡易ユーザー登録アプリは、ざっくり言うと次の責務を持っています。

  • 入力の管理
    • 名前・メールの入力値をStateで管理する
  • バリデーション
    • 名前:必須・最大文字数
    • メール:必須・簡易形式チェック
  • 登録処理
    • バリデーションを通過したユーザーを一覧に追加する
  • 一覧表示
    • 登録済みユーザーを画面に表示する
  • 削除処理
    • 一覧から特定のユーザーを取り除く

中盤では、この責務を意識しながら、 コードの構造を少しだけ整理していきます。

ユーザーオブジェクトの形をはっきり決める

「ユーザーとは何か」をコードで表現する

前半では、ユーザーオブジェクトを次のように扱いました。

const newUser = {
  id: Date.now(),
  name,
  email,
};
JSX

ここでの id は「一意な識別子」として使っています。 中盤では、この「ユーザーの形」をもう少し意識してみます。

  • id:ユーザーを一意に識別するための値
  • name:表示名
  • email:連絡先・ログインID候補

この3つが揃っていることで、

  • 一覧表示で「誰が誰か」を区別できる
  • 削除処理で「どのユーザーを消すか」を特定できる

という状態になります。

重要な感覚:

「ユーザーオブジェクトの形」を自分の中で言葉にしておくと、 Stateの配列を扱うときに迷いにくくなります。

バリデーションと登録処理を「読みやすく」する

handleSubmitの中身を整理する

前半の handleSubmit は、すでに機能としては十分ですが、 中盤では「読みやすさ」を意識して少しだけ整理してみます。

const handleSubmit = (e) => {
  e.preventDefault();

  // 1. エラーをクリア
  setNameError("");
  setEmailError("");

  // 2. バリデーション
  const nameValidationResult = validateName(name);
  const emailValidationResult = validateEmail(email);

  const hasError =
    nameValidationResult !== "" || emailValidationResult !== "";

  if (nameValidationResult !== "") {
    setNameError(nameValidationResult);
  }

  if (emailValidationResult !== "") {
    setEmailError(emailValidationResult);
  }

  if (hasError) {
    // エラーがあるので登録しない
    return;
  }

  // 3. ユーザーオブジェクトの作成
  const newUser = {
    id: Date.now(),
    name,
    email,
  };

  // 4. ユーザー一覧に追加
  setUsers((prevUsers) => [...prevUsers, newUser]);

  // 5. 入力欄をリセット
  setName("");
  setEmail("");
};
JSX

ここでのポイントは、

  • 「エラーがあるかどうか」を hasError という変数で表現していること
  • handleSubmit の中身を「1. エラークリア → 2. バリデーション → 3. ユーザー作成 → 4. 追加 → 5. リセット」という流れで整理していること

です。

重要な感覚:

フォーム送信処理は、「何をしているか」が一目で分かるように ステップごとに整理して書くと、後から読み返したときに理解しやすくなります。

削除処理を「Stateの更新」として理解する

filterで「残すものだけ」を選ぶ

削除処理は、次のように書いていました。

const handleDeleteUser = (id) => {
  setUsers((prevUsers) => prevUsers.filter((user) => user.id !== id));
};
JSX

ここで起きていることを言葉で整理すると、

  • prevUsers は「今のユーザー一覧」
  • filter は「条件を満たす要素だけを残す」関数
  • user.id !== id という条件で、「削除したいユーザー以外」を残している

つまり、

「削除したいユーザーを取り除いた新しい配列」を作り、 それを setUsers に渡している

ということになります。

重要な感覚:

ReactのStateは「直接書き換えず、新しい値を作って置き換える」というルールがあります。 削除処理も、「新しい配列を作ってStateを更新する」という形で考えると整理しやすくなります。

一覧表示のUIを少しだけ整える

見やすさと操作しやすさを意識する

中盤では、一覧表示のUIを少しだけ整えてみます。

<div style={{ marginTop: "24px" }}>
  <h3>登録済みユーザー一覧</h3>
  {users.length === 0 ? (
    <p>まだユーザーは登録されていません。</p>
  ) : (
    <ul style={{ paddingLeft: "16px" }}>
      {users.map((user) => (
        <li key={user.id} style={{ marginBottom: "4px" }}>
          <span>
            {user.name}{user.email}
          </span>
          <button
            style={{ marginLeft: "8px" }}
            onClick={() => handleDeleteUser(user.id)}
          >
            削除
          </button>
        </li>
      ))}
    </ul>
  )}
</div>
JSX

ここでのポイントは、

  • marginBottompaddingLeft で、一覧の見やすさを少しだけ改善していること
  • 名前とメールを span でまとめて表示していること
  • 削除ボタンを横に並べて、「どのユーザーを消すか」が直感的に分かるようにしていること

です。

重要な感覚:

UIのちょっとした調整でも、 「ユーザーが迷わず操作できるかどうか」が変わってきます。 コードを書きながら、画面の見え方も意識していくと良いです。

DAY 36 中盤版テンプレート ― 少し整理された簡易ユーザー登録アプリ

ここまでの内容をすべて含んだ、 DAY 36中盤のテンプレートをまとめます。

import { useState } from "react";

const MAX_NAME_LENGTH = 20;

function SimpleUserRegisterAppMid() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  const [nameError, setNameError] = useState("");
  const [emailError, setEmailError] = useState("");

  const [users, setUsers] = useState([]);

  const validateName = (name) => {
    if (name.trim() === "") {
      return "名前は必須です。";
    }

    if (name.length > MAX_NAME_LENGTH) {
      return `名前は${MAX_NAME_LENGTH}文字以内で入力してください。`;
    }

    return "";
  };

  const validateEmail = (email) => {
    if (email.trim() === "") {
      return "メールアドレスは必須です。";
    }

    if (!email.includes("@")) {
      return "メールアドレスの形式が正しくありません。";
    }

    return "";
  };

  const handleSubmit = (e) => {
    e.preventDefault();

    setNameError("");
    setEmailError("");

    const nameValidationResult = validateName(name);
    const emailValidationResult = validateEmail(email);

    const hasError =
      nameValidationResult !== "" || emailValidationResult !== "";

    if (nameValidationResult !== "") {
      setNameError(nameValidationResult);
    }

    if (emailValidationResult !== "") {
      setEmailError(emailValidationResult);
    }

    if (hasError) {
      return;
    }

    const newUser = {
      id: Date.now(),
      name,
      email,
    };

    setUsers((prevUsers) => [...prevUsers, newUser]);

    setName("");
    setEmail("");
  };

  const handleDeleteUser = (id) => {
    setUsers((prevUsers) => prevUsers.filter((user) => user.id !== id));
  };

  return (
    <div>
      <h2>DAY 36:フォーム総合演習中盤 ― 簡易ユーザー登録アプリ(整理版)</h2>

      <form onSubmit={handleSubmit}>
        <div>
          <label>
            名前:
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="名前を入力してください"
            />
          </label>
          {nameError && (
            <p style={{ color: "red", marginTop: "4px" }}>{nameError}</p>
          )}
        </div>

        <div style={{ marginTop: "8px" }}>
          <label>
            メール:
            <input
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              placeholder="メールアドレスを入力してください"
            />
          </label>
          {emailError && (
            <p style={{ color: "red", marginTop: "4px" }}>{emailError}</p>
          )}
        </div>

        <div style={{ marginTop: "12px" }}>
          <button type="submit">登録</button>
        </div>
      </form>

      <div style={{ marginTop: "24px" }}>
        <h3>登録済みユーザー一覧</h3>
        {users.length === 0 ? (
          <p>まだユーザーは登録されていません。</p>
        ) : (
          <ul style={{ paddingLeft: "16px" }}>
            {users.map((user) => (
              <li key={user.id} style={{ marginBottom: "4px" }}>
                <span>
                  {user.name}{user.email}
                </span>
                <button
                  style={{ marginLeft: "8px" }}
                  onClick={() => handleDeleteUser(user.id)}
                >
                  削除
                </button>
              </li>
            ))}
          </ul>
        )}
      </div>
    </div>
  );
}

export default SimpleUserRegisterAppMid;
JSX

DAY 36 中盤のまとめ ― 「流れ+構造」を同時に意識できるようになる

DAY 36中盤で定着させてほしいポイントは次の通りです。

  • 入力 → バリデーション → 登録 → 一覧表示 → 削除 という流れを、1つのコンポーネントの中で整理して書けること。
  • ユーザーオブジェクトの形(id, name, email)を意識することで、一覧表示や削除処理が理解しやすくなること。
  • 削除処理は、「新しい配列を作ってStateを更新する」というReactの基本ルールに沿って考えること。

この「流れ+構造」を同時に意識できる感覚が身につくと、 この先のより大きなアプリでも、 フォームまわりの設計を落ち着いて進められるようになっていきます。


DAY 36:フォーム総合演習 後半 ― 「実戦を意識した簡易ユーザー登録アプリ」に仕上げる

DAY 36の後半では、前半・中盤で作ってきた簡易ユーザー登録アプリを、「実戦を意識した形」に一歩近づけていきます。 すでに、

  • 入力
  • バリデーション
  • 登録
  • 一覧表示
  • 削除

という一連の流れはできていますので、後半ではそれを「使いやすく・安全に・読みやすく」する方向で整えていきます。

アプリ全体の流れをもう一度確認する

入力から削除までの一連のストーリー

この簡易ユーザー登録アプリは、次のようなストーリーで動きます。

  1. ユーザーが「名前」と「メール」を入力する(入力)
  2. 送信ボタンを押すと、バリデーションが走る(バリデーション)
  3. バリデーションを通過したら、ユーザー一覧に追加される(登録)
  4. 画面下部に登録済みユーザー一覧が表示される(一覧表示)
  5. 各ユーザーの横にある「削除」ボタンで、そのユーザーを一覧から消せる(削除)

後半では、この流れを保ちつつ、

  • ユーザー体験(UX)
  • コードの見通し
  • セキュリティの入り口としての意識

を少しだけ強化していきます。

入力とバリデーションを「ユーザー目線」で整える

入力欄にプレースホルダーとラベルを丁寧に付ける

まず、入力欄のUIを少し整えます。 ユーザーが迷わず入力できるように、ラベルとプレースホルダーを丁寧に付けます。

<form onSubmit={handleSubmit}>
  <div>
    <label>
      名前:
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="例:山田太郎"
      />
    </label>
    {nameError && (
      <p style={{ color: "red", marginTop: "4px" }}>{nameError}</p>
    )}
  </div>

  <div style={{ marginTop: "8px" }}>
    <label>
      メール:
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="例:taro@example.com"
      />
    </label>
    {emailError && (
      <p style={{ color: "red", marginTop: "4px" }}>{emailError}</p>
    )}
  </div>

  <div style={{ marginTop: "12px" }}>
    <button type="submit">登録</button>
  </div>
</form>
JSX

ここでのポイントは、

  • プレースホルダーに「例」を書くことで、ユーザーが入力イメージを持ちやすくなること
  • エラーメッセージを入力欄のすぐ下に表示し、「どこが問題か」を即座に伝えていること

です。

バリデーションを「実戦の入り口」として意識する

名前・メールのルールを明確にする

バリデーション関数は、アプリの「入口のフィルター」です。 ここで「どんな入力を許すか・許さないか」を決めます。

const MAX_NAME_LENGTH = 20;

const validateName = (name) => {
  if (name.trim() === "") {
    return "名前は必須です。";
  }

  if (name.length > MAX_NAME_LENGTH) {
    return `名前は${MAX_NAME_LENGTH}文字以内で入力してください。`;
  }

  return "";
};

const validateEmail = (email) => {
  if (email.trim() === "") {
    return "メールアドレスは必須です。";
  }

  if (!email.includes("@")) {
    return "メールアドレスの形式が正しくありません。";
  }

  return "";
};
JSX

ここで意識してほしいのは、

  • 「必須」「文字数」「形式」といったルールを、コードの中で明確に表現していること
  • エラーメッセージに「どう直せばよいか」が分かる情報を含めていること

です。

重要な感覚:

バリデーションは「ユーザーを困らせるための壁」ではなく、 「アプリを守りつつ、ユーザーに正しい入力を促すためのガイド」です。

登録処理と一覧表示を「安全に・分かりやすく」つなぐ

ユーザーオブジェクトの扱い方を整理する

登録処理では、バリデーションを通過した入力を「ユーザーオブジェクト」として一覧に追加します。

const handleSubmit = (e) => {
  e.preventDefault();

  setNameError("");
  setEmailError("");

  const nameValidationResult = validateName(name);
  const emailValidationResult = validateEmail(email);

  const hasError =
    nameValidationResult !== "" || emailValidationResult !== "";

  if (nameValidationResult !== "") {
    setNameError(nameValidationResult);
  }

  if (emailValidationResult !== "") {
    setEmailError(emailValidationResult);
  }

  if (hasError) {
    return;
  }

  const newUser = {
    id: Date.now(), // 簡易的な一意ID
    name,
    email,
  };

  setUsers((prevUsers) => [...prevUsers, newUser]);

  setName("");
  setEmail("");
};
JSX

ここでのポイントは、

  • hasError という変数で「エラーがあるかどうか」を明示していること
  • ユーザーオブジェクトに id を持たせることで、一覧表示や削除処理で「誰が誰か」を区別できること
  • 登録後に入力欄をリセットし、次の入力に備えていること

です。

一覧表示は、ユーザーオブジェクトの配列をそのまま画面に反映します。

<div style={{ marginTop: "24px" }}>
  <h3>登録済みユーザー一覧</h3>
  {users.length === 0 ? (
    <p>まだユーザーは登録されていません。</p>
  ) : (
    <ul style={{ paddingLeft: "16px" }}>
      {users.map((user) => (
        <li key={user.id} style={{ marginBottom: "4px" }}>
          <span>
            {user.name}{user.email}
          </span>
          <button
            style={{ marginLeft: "8px" }}
            onClick={() => handleDeleteUser(user.id)}
          >
            削除
          </button>
        </li>
      ))}
    </ul>
  )}
</div>
JSX

削除処理を「State更新の基本パターン」として理解する

filterで「残すものだけ」を選ぶ

削除処理は、ReactのState更新の基本パターンを体現しています。

const handleDeleteUser = (id) => {
  setUsers((prevUsers) => prevUsers.filter((user) => user.id !== id));
};
JSX

ここで起きていることは、

  • prevUsers は「今のユーザー一覧」
  • filter は「条件を満たす要素だけを残す」関数
  • user.id !== id という条件で、「削除したいユーザー以外」を残している

つまり、

「削除したいユーザーを取り除いた新しい配列」を作り、 それを setUsers に渡している

ということです。

重要な感覚:

Reactでは「Stateを直接書き換えず、新しい値を作って置き換える」というルールがあります。 削除処理も、このルールに沿って「新しい配列を作る」という形で考えると、自然に理解できます。

DAY 36 後半版テンプレート ― 実戦寄り簡易ユーザー登録アプリ

ここまでの内容をすべて含んだ、DAY 36後半のテンプレートをまとめます。

import { useState } from "react";

const MAX_NAME_LENGTH = 20;

function SimpleUserRegisterAppFinal() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  const [nameError, setNameError] = useState("");
  const [emailError, setEmailError] = useState("");

  const [users, setUsers] = useState([]);

  const validateName = (name) => {
    if (name.trim() === "") {
      return "名前は必須です。";
    }

    if (name.length > MAX_NAME_LENGTH) {
      return `名前は${MAX_NAME_LENGTH}文字以内で入力してください。`;
    }

    return "";
  };

  const validateEmail = (email) => {
    if (email.trim() === "") {
      return "メールアドレスは必須です。";
    }

    if (!email.includes("@")) {
      return "メールアドレスの形式が正しくありません。";
    }

    return "";
  };

  const handleSubmit = (e) => {
    e.preventDefault();

    setNameError("");
    setEmailError("");

    const nameValidationResult = validateName(name);
    const emailValidationResult = validateEmail(email);

    const hasError =
      nameValidationResult !== "" || emailValidationResult !== "";

    if (nameValidationResult !== "") {
      setNameError(nameValidationResult);
    }

    if (emailValidationResult !== "") {
      setEmailError(emailValidationResult);
    }

    if (hasError) {
      return;
    }

    const newUser = {
      id: Date.now(),
      name,
      email,
    };

    setUsers((prevUsers) => [...prevUsers, newUser]);

    setName("");
    setEmail("");
  };

  const handleDeleteUser = (id) => {
    setUsers((prevUsers) => prevUsers.filter((user) => user.id !== id));
  };

  return (
    <div>
      <h2>DAY 36:フォーム総合演習後半 ― 簡易ユーザー登録アプリ完成版</h2>

      <form onSubmit={handleSubmit}>
        <div>
          <label>
            名前:
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="例:山田太郎"
            />
          </label>
          {nameError && (
            <p style={{ color: "red", marginTop: "4px" }}>{nameError}</p>
          )}
        </div>

        <div style={{ marginTop: "8px" }}>
          <label>
            メール:
            <input
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              placeholder="例:taro@example.com"
            />
          </label>
          {emailError && (
            <p style={{ color: "red", marginTop: "4px" }}>{emailError}</p>
          )}
        </div>

        <div style={{ marginTop: "12px" }}>
          <button type="submit">登録</button>
        </div>
      </form>

      <div style={{ marginTop: "24px" }}>
        <h3>登録済みユーザー一覧</h3>
        {users.length === 0 ? (
          <p>まだユーザーは登録されていません。</p>
        ) : (
          <ul style={{ paddingLeft: "16px" }}>
            {users.map((user) => (
              <li key={user.id} style={{ marginBottom: "4px" }}>
                <span>
                  {user.name}{user.email}
                </span>
                <button
                  style={{ marginLeft: "8px" }}
                  onClick={() => handleDeleteUser(user.id)}
                >
                  削除
                </button>
              </li>
            ))}
          </ul>
        )}
      </div>
    </div>
  );
}

export default SimpleUserRegisterAppFinal;
JSX

DAY 36 後半のまとめ ― 「フォームの一連の流れ」を自分の手で完結させる

DAY 36後半で定着させてほしいポイントは次の通りです。

  • 入力 → バリデーション → 登録 → 一覧表示 → 削除 という一連の流れを、1つのコンポーネントの中で完結させられること。
  • バリデーションは、アプリを守りつつユーザーに正しい入力を促す「入口のフィルター」であること。
  • 削除処理は、「新しい配列を作ってStateを更新する」というReactの基本パターンであること。

このミニアプリの流れが腑に落ちてくると、 より大きなアプリでも、フォームまわりの設計を落ち着いて進められるようになっていきます。

タイトルとURLをコピーしました