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

React 60日で身につけるReact
スポンサーリンク
スポンサーリンク
  1. DAY 37:コンポーネント設計 前半 ― 「責務」を軸に考える練習を始める
  2. コンポーネントの責務を理解する
    1. コンポーネントの責務とは何か
    2. 例:責務を意識しないコンポーネント
    3. 責務を分けて考えるとどうなるか
  3. コンポーネントを分割する理由
    1. なぜわざわざ分割するのか
  4. 大きすぎるコンポーネントとは何か
    1. 「何でも屋コンポーネント」の問題
    2. 例:大きすぎるコンポーネント
    3. 大きすぎるコンポーネントのデメリット
  5. 小さすぎるコンポーネントとは何か
    1. 「分割しすぎ」もまた問題になる
    2. 例:小さすぎるコンポーネント
    3. 小さすぎるコンポーネントのデメリット
  6. ちょうどよいコンポーネント設計のイメージ
    1. 1コンポーネント=1責務を意識する
  7. DAY 37 前半のまとめ ― 「責務」を軸にコンポーネントを見る目を育てる
  8. DAY 37:コンポーネント設計 中盤 ― 実際のコードで「責務」と「分割」を体に染み込ませる
  9. コンポーネントの責務をコードで意識する
    1. まずは「全部入りコンポーネント」からスタートする
  10. 分割する理由を「実際の分割」で体感する
    1. 責務ごとにコンポーネントを分けてみる
    2. 親コンポーネント:UserPage(全体の責務)
  11. 大きすぎるコンポーネントを「ちょうどよい大きさ」にする
    1. UserForm:入力+バリデーション+登録ボタンに責務を絞る
  12. 小さすぎるコンポーネントにならないようにする
    1. UserListとUserItemの分割バランスを考える
  13. 分割後の全体像をもう一度眺める
    1. 責務ごとにコンポーネントが整理された状態
  14. DAY 37 中盤のまとめ ― 「分割の感覚」をコードで身につける
  15. DAY 37:コンポーネント設計 後半 ― 「自分で設計判断できる」レベルに一段階引き上げる
  16. コンポーネントの責務を「言葉で説明できるか」を基準にする
    1. 責務は「一文で説明できるか」をチェックすると分かりやすいです
    2. 責務を言葉にすること自体が設計の練習になります
  17. 分割する理由を「未来の自分」の視点で考える
    1. 今の自分ではなく「半年後の自分」が読むコードだと思ってみる
    2. 分割の判断軸をいくつか持っておくと迷いにくくなります
  18. 大きすぎるコンポーネントを見抜く「違和感センサー」を育てる
    1. 読んでいて「息切れする」コンポーネントは大きすぎることが多いです
    2. 実際に「分割前」と「分割後」を比べてみる
  19. 小さすぎるコンポーネントを避ける「意味のある単位」の感覚
    1. 「意味のある単位」かどうかを自分に問いかける
    2. 小さすぎるコンポーネントが増えると何が起きるか
  20. DAY 37 後半版テンプレート ― 設計判断のためのチェックリスト
    1. コンポーネントの責務チェック
    2. 分割する理由チェック
    3. 大きすぎるコンポーネントチェック
    4. 小さすぎるコンポーネントチェック
  21. DAY 37 後半のまとめ ― 「設計の目」を持ったReact開発者への一歩

DAY 37:コンポーネント設計 前半 ― 「責務」を軸に考える練習を始める

DAY 37は、フォームだけでなくReactアプリ全体の設計力に関わる、とても重要な回になります。 ここでは次の4つをテーマに、コンポーネント設計の「考え方の土台」をつくっていきます。

  • コンポーネントの責務
  • 分割する理由
  • 大きすぎるコンポーネント
  • 小さすぎるコンポーネント

コードを書く前に「どう分けるか」を考えられるようになると、 アプリの規模が大きくなっても、落ち着いて設計できるようになります。

コンポーネントの責務を理解する

コンポーネントの責務とは何か

コンポーネントの責務とは、簡単に言うと

「このコンポーネントは何を担当するのか」という役割のこと

です。

例えば、簡易ユーザー登録アプリを思い出してみると、次のような役割がありました。

  • 入力フォームを表示する
  • 入力値をStateで管理する
  • バリデーションを行う
  • 登録済みユーザー一覧を表示する
  • 削除ボタンでユーザーを消す

これらを1つのコンポーネントが全部やるのか、 それとも複数のコンポーネントに分けて担当させるのかが、設計のポイントになります。

例:責務を意識しないコンポーネント

まず、責務を意識せずに書いた「全部入りコンポーネント」の例を見てみます。

import { useState } from "react";

function UserPage() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [nameError, setNameError] = useState("");
  const [emailError, setEmailError] = useState("");
  const [users, setUsers] = useState([]);

  const handleSubmit = (e) => { /* 入力チェック+登録 */ };
  const handleDeleteUser = (id) => { /* 削除 */ };

  return (
    <div>
      <h2>ユーザー登録ページ</h2>

      {/* 入力フォーム */}
      {/* エラーメッセージ */}
      {/* 登録ボタン */}
      {/* 一覧表示 */}
      {/* 削除ボタン */}
    </div>
  );
}

export default UserPage;
JSX

一見すると「動きそう」ですが、 このコンポーネントは責務をたくさん抱えすぎている状態です。

責務を分けて考えるとどうなるか

同じ機能を、責務ごとに分けて考えると次のようになります。

  • <UserForm />
    • 入力欄(名前・メール)
    • バリデーション
    • 「登録」ボタン
  • <UserList />
    • 登録済みユーザー一覧の表示
  • <UserItem />
    • 1ユーザーの表示
    • 「削除」ボタン

これらをまとめる親コンポーネントとして、 <UserPage /> が「全体の状態管理」を担当する、という形にできます。

コンポーネントを分割する理由

なぜわざわざ分割するのか

コンポーネントを分割する理由は、主に次の4つです。

1. 読みやすくするため

1つのコンポーネントに機能を詰め込みすぎると、 「何をしているのか」が一目で分からなくなります。

分割すると、

  • <UserForm /> を見れば「入力とバリデーション」
  • <UserList /> を見れば「一覧表示」

というように、読むときの負担が減ります

2. 修正しやすくするため

例えば、「バリデーションのルールだけ変えたい」ときに、 フォーム部分と一覧部分が同じコンポーネントに混ざっていると、 どこを触ればいいか分かりづらくなります。

<UserForm /> にバリデーションを閉じ込めておけば、 フォームだけを修正すればよいので、影響範囲が小さくなります。

3. 再利用しやすくするため

「ユーザー一覧表示」は、別の画面でも使えるかもしれません。

  • 管理画面
  • 検索結果画面
  • ダッシュボード

などで、同じような一覧表示が必要になることがあります。

<UserList /> として切り出しておけば、 別のページでもそのまま使い回せる可能性が高くなります。

4. テストしやすくするため

小さなコンポーネントは、テストがしやすいです。

  • <UserForm /> のテスト:
    • 入力 → バリデーション → 登録ボタンの動き
  • <UserList /> のテスト:
    • users配列を渡したときの表示内容

責務ごとに分かれていると、 「このコンポーネントは何をテストすべきか」が明確になります。

大きすぎるコンポーネントとは何か

「何でも屋コンポーネント」の問題

大きすぎるコンポーネントは、 いわば「何でも屋コンポーネント」です。

特徴としては、

  • Stateがたくさんある
  • 関数がたくさんある
  • JSXが長くてスクロールしないと全体が見えない
  • 読んでいて「どこからどこまでが何の処理か」分かりづらい

という状態になります。

例:大きすぎるコンポーネント

function BigUserComponent() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [nameError, setNameError] = useState("");
  const [emailError, setEmailError] = useState("");
  const [users, setUsers] = useState([]);

  const handleSubmit = (e) => { /* 入力+バリデーション+登録 */ };
  const handleDeleteUser = (id) => { /* 削除 */ };

  return (
    <div>
      {/* 入力フォーム */}
      {/* エラーメッセージ */}
      {/* 登録ボタン */}
      {/* 一覧表示 */}
      {/* 削除ボタン */}
      {/* さらに別のUIが増えていく… */}
    </div>
  );
}
JSX

このコンポーネントは、

  • 入力フォーム
  • バリデーション
  • 登録処理
  • 一覧表示
  • 削除処理

すべてを抱えています。

大きすぎるコンポーネントのデメリット

  • 読みづらい:何をしているのか一目で分からない
  • 修正しづらい:1箇所を直したつもりが、別の機能に影響することがある
  • 再利用できない:一部だけ使いたくても、切り出しにくい
  • バグが潜みやすい:責務が多いほど、見落としが増える

初心者のうちは「とりあえず1コンポーネントに全部書く」ことが多いですが、 DAY 37ではそこから一歩進んで、「どこで分けるか」を考える練習をしていきます。

小さすぎるコンポーネントとは何か

「分割しすぎ」もまた問題になる

逆に、コンポーネントを細かく分けすぎると、

  • コンポーネントの数が増えすぎる
  • どこに何があるか分からない
  • 1つ1つのコンポーネントの意味が薄くなる

という問題が起きます。

例:小さすぎるコンポーネント

function NameLabel() {
  return <label>名前:</label>;
}

function NameInput({ value, onChange }) {
  return <input type="text" value={value} onChange={onChange} />;
}

function EmailLabel() {
  return <label>メール:</label>;
}

function EmailInput({ value, onChange }) {
  return <input type="email" value={value} onChange={onChange} />;
}

function SubmitButton() {
  return <button>送信</button>;
}
JSX

これらは、責務が小さすぎる例です。

小さすぎるコンポーネントのデメリット

  • コンポーネントの数が増えすぎて、管理が大変になる
  • 「このコンポーネントは何を担当しているのか」が逆に分かりづらくなる
  • 再利用性もそれほど高くない(ラベルや単純なinputだけでは、場面ごとに微妙に違うことが多い)

つまり、「分ければ分けるほど良い」というわけではないのです。

ちょうどよいコンポーネント設計のイメージ

1コンポーネント=1責務を意識する

ちょうどよいコンポーネント設計の基本は、

1コンポーネント=1責務

です。

簡易ユーザー登録アプリを例にすると、次のような分け方が「ちょうどよい」イメージになります。

function UserPage() {
  // 全体のState管理(usersなど)
  // UserFormとUserListを組み合わせる
}

function UserForm({ onAddUser }) {
  // 入力+バリデーション+登録ボタン
}

function UserList({ users, onDeleteUser }) {
  // 一覧表示
}

function UserItem({ user, onDelete }) {
  // 1ユーザーの表示+削除ボタン
}
JSX
  • <UserForm /> の責務: 「ユーザーを1人登録するための入力とバリデーション」
  • <UserList /> の責務: 「登録済みユーザー一覧を表示する」
  • <UserItem /> の責務: 「1人分のユーザー情報を表示し、削除できるようにする」

このように、コンポーネントごとに「何を担当するか」を言葉で説明できる状態が理想です。

DAY 37 前半のまとめ ― 「責務」を軸にコンポーネントを見る目を育てる

DAY 37前半で身につけてほしいポイントは次の通りです。

  • コンポーネントの責務とは、「そのコンポーネントが何を担当するか」という役割のことです。
  • 分割する理由は、「読みやすさ」「修正しやすさ」「再利用性」「テストのしやすさ」を高めるためです。
  • 大きすぎるコンポーネントは、責務を抱えすぎていて、読みにくく・修正しにくく・バグが潜みやすくなります。
  • 小さすぎるコンポーネントは、分割しすぎで逆に分かりづらくなり、管理が大変になります。
  • 目指すのは「1コンポーネント=1責務」。コンポーネントごとに「このコンポーネントは何を担当しているのか」を言葉で説明できる状態です。

この「責務を軸にコンポーネントを見る目」が育ってくると、 DAY 36までに作ってきたフォームアプリを、 より読みやすく・より拡張しやすい構造に育てていくことができるようになります。


DAY 37:コンポーネント設計 中盤 ― 実際のコードで「責務」と「分割」を体に染み込ませる

DAY 37の中盤では、前半で学んだ

  • コンポーネントの責務
  • 分割する理由
  • 大きすぎるコンポーネント
  • 小さすぎるコンポーネント

を、実際のコードを使って「手触りのある理解」に変えていくことを目指します。 ここでは、これまで作ってきた「簡易ユーザー登録アプリ」を題材に、 コンポーネントをどう分割するかをステップバイステップで考えていきます。

コンポーネントの責務をコードで意識する

まずは「全部入りコンポーネント」からスタートする

簡易ユーザー登録アプリを、あえて「全部入りコンポーネント」として書いてみます。

import { useState } from "react";

function UserPage() {
  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 > 20) {
      return "名前は20文字以内で入力してください。";
    }
    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>ユーザー登録ページ(全部入り版)</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 UserPage;
JSX

このコンポーネントは、次の責務をすべて抱えています。

  • 入力フォームの表示
  • 入力値のState管理
  • バリデーション
  • 登録処理
  • 一覧表示
  • 削除処理

まさに「何でも屋コンポーネント」です。

分割する理由を「実際の分割」で体感する

責務ごとにコンポーネントを分けてみる

ここから、責務ごとにコンポーネントを分割していきます。 目標は次の構成です。

  • <UserPage />:全体の状態管理
  • <UserForm />:入力+バリデーション+登録ボタン
  • <UserList />:一覧表示
  • <UserItem />:1ユーザーの表示+削除ボタン

親コンポーネント:UserPage(全体の責務)

まず、親コンポーネントを「全体の状態管理」に集中させます。

import { useState } from "react";
import UserForm from "./UserForm";
import UserList from "./UserList";

function UserPage() {
  const [users, setUsers] = useState([]);

  const handleAddUser = (user) => {
    setUsers((prevUsers) => [...prevUsers, user]);
  };

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

  return (
    <div>
      <h2>ユーザー登録ページ(分割版)</h2>

      <UserForm onAddUser={handleAddUser} />

      <div style={{ marginTop: "24px" }}>
        <UserList users={users} onDeleteUser={handleDeleteUser} />
      </div>
    </div>
  );
}

export default UserPage;
JSX

ここでの責務は、

  • users のState管理
  • ユーザー追加処理
  • ユーザー削除処理
  • <UserForm /><UserList /> を組み合わせる

に絞られています。

重要なポイント:

親コンポーネントは「データの持ち主」として振る舞い、 子コンポーネントに「表示」と「操作」を任せる形にすると、構造がすっきりします。

大きすぎるコンポーネントを「ちょうどよい大きさ」にする

UserForm:入力+バリデーション+登録ボタンに責務を絞る

次に、フォーム部分を <UserForm /> として切り出します。

import { useState } from "react";

const MAX_NAME_LENGTH = 20;

function UserForm({ onAddUser }) {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [nameError, setNameError] = useState("");
  const [emailError, setEmailError] = 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,
    };

    onAddUser(newUser);

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

  return (
    <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>
  );
}

export default UserForm;
JSX

ここでの責務は、

  • 入力欄の表示
  • 入力値のState管理
  • バリデーション
  • 「登録」ボタンのクリック処理

に絞られています。

重要なポイント:

<UserForm /> は「1人のユーザーを登録するためのフォーム」という責務に集中しています。 ユーザー一覧のことは一切知らず、「登録されたユーザーを親に渡す」だけにしているのがポイントです。

小さすぎるコンポーネントにならないようにする

UserListとUserItemの分割バランスを考える

一覧表示部分を <UserList /><UserItem /> に分けます。

import UserItem from "./UserItem";

function UserList({ users, onDeleteUser }) {
  if (users.length === 0) {
    return <p>まだユーザーは登録されていません。</p>;
  }

  return (
    <div>
      <h3>登録済みユーザー一覧</h3>
      <ul style={{ paddingLeft: "16px" }}>
        {users.map((user) => (
          <UserItem
            key={user.id}
            user={user}
            onDelete={() => onDeleteUser(user.id)}
          />
        ))}
      </ul>
    </div>
  );
}

export default UserList;
JSX

<UserItem /> は、1ユーザーの表示+削除ボタンを担当します。

function UserItem({ user, onDelete }) {
  return (
    <li style={{ marginBottom: "4px" }}>
      <span>
        {user.name}{user.email}
      </span>
      <button style={{ marginLeft: "8px" }} onClick={onDelete}>
        削除
      </button>
    </li>
  );
}

export default UserItem;
JSX

ここでの分割は、

  • <UserList />:一覧全体の表示
  • <UserItem />:1行分の表示+削除ボタン

という「ちょうどよい責務の分け方」になっています。

重要なポイント:

<UserItem /> は「ラベルだけ」「inputだけ」のような小さすぎるコンポーネントではなく、 「1ユーザーの表示と削除」という意味のある単位を担当しています。 これが「小さすぎないコンポーネント」の良い例です。

分割後の全体像をもう一度眺める

責務ごとにコンポーネントが整理された状態

分割後の構成は次のようになります。

  • <UserPage />
    • usersのState管理
    • ユーザー追加・削除処理
    • <UserForm /><UserList /> の組み合わせ
  • <UserForm />
    • 入力欄
    • バリデーション
    • 登録ボタン
    • 登録されたユーザーを親に渡す
  • <UserList />
    • users配列を受け取り、一覧として表示する
  • <UserItem />
    • 1ユーザーの表示
    • 削除ボタン

この状態になると、

  • どのコンポーネントが何を担当しているか、言葉で説明しやすくなる
  • 修正したいときに「どこを触ればよいか」が分かりやすくなる
  • 一覧表示部分だけを別の画面で再利用することも現実的になる

というメリットが生まれます。

DAY 37 中盤のまとめ ― 「分割の感覚」をコードで身につける

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

  • まず「全部入りコンポーネント」を書いてみてから、「責務ごとに分ける」練習をすると、分割の意味が体感しやすくなること。
  • 親コンポーネントは「データの持ち主」として振る舞い、子コンポーネントに表示や操作を任せる構造が読みやすさにつながること。
  • <UserForm /> のように、「1人のユーザーを登録するフォーム」という明確な責務を持たせると、バリデーションや入力処理が整理されること。
  • <UserList /><UserItem /> のように、「一覧全体」と「1行分」という単位で分けると、小さすぎず大きすぎないコンポーネント設計になること。

この「分割の感覚」が少しずつ身についてくると、 今後作るフォームや画面でも、 「どこでコンポーネントを分けるべきか」を自然に考えられるようになっていきます。


DAY 37:コンポーネント設計 後半 ― 「自分で設計判断できる」レベルに一段階引き上げる

DAY 37の後半では、前半・中盤で学んだ

  • コンポーネントの責務
  • 分割する理由
  • 大きすぎるコンポーネント
  • 小さすぎるコンポーネント

を、「自分で設計判断できる」レベルに引き上げることを目標にします。 ここからは、少しだけ抽象度を上げて、設計の考え方そのものを丁寧に言語化していきます。

コンポーネントの責務を「言葉で説明できるか」を基準にする

責務は「一文で説明できるか」をチェックすると分かりやすいです

コンポーネントの責務が明確かどうかを確認する簡単な方法は、

「このコンポーネントは何をするものですか?」と聞かれたときに、 一文で説明できるかどうか

をチェックすることです。

例えば、次のように言えれば責務はかなり明確です。

  • UserForm: 「ユーザーを1人登録するための入力フォームとバリデーションを担当します。」
  • UserList: 「登録済みユーザー一覧を表示し、削除操作を受け取れるようにします。」
  • UserItem: 「1人分のユーザー情報を表示し、そのユーザーを削除するボタンを持ちます。」

逆に、説明がこうなってしまうと危険信号です。

  • 「入力もやって、一覧も出して、削除もして、あとモーダルも開いて…」

このように説明が長くなるときは、責務を抱えすぎている可能性が高いです。

責務を言葉にすること自体が設計の練習になります

コードを書く前に、次のように紙やメモに書き出してみるのもおすすめです。

  • UserPage: 「ユーザー一覧の状態を管理し、フォームと一覧をつなぐ親コンポーネント」
  • UserForm: 「ユーザー登録のための入力とバリデーションを担当するコンポーネント」
  • UserList: 「ユーザー一覧を表示するコンポーネント」
  • UserItem: 「1ユーザーの表示と削除ボタンを担当するコンポーネント」

この「言葉にする」プロセスが、 コンポーネント設計の精度を自然と高めてくれます。

分割する理由を「未来の自分」の視点で考える

今の自分ではなく「半年後の自分」が読むコードだと思ってみる

コンポーネントを分割するかどうか迷ったときは、

「半年後の自分がこのコードを読むとしたら、どう分かれていてほしいか」

という視点で考えると判断しやすくなります。

半年後の自分は、

  • すべての細かい実装を覚えていない
  • でも、アプリのざっくりした構造は覚えているかもしれない
  • バグ修正や機能追加のために、どこを触ればいいか素早く知りたい

という状態になっていることが多いです。

そのときに、

  • 「フォームのことは UserForm を見れば分かる」
  • 「一覧表示のことは UserList を見れば分かる」

という構造になっていると、未来の自分がかなり楽になります

分割の判断軸をいくつか持っておくと迷いにくくなります

コンポーネントを分割するかどうかの判断軸として、次のようなものを持っておくと便利です。

  • 役割が2つ以上あるか
    • 入力+一覧表示+モーダル表示…など、明らかに複数の責務を持っているなら分割候補です。
  • Stateが多すぎないか
    • 10個以上のStateを1コンポーネントで持っている場合、責務を分けられる可能性が高いです。
  • JSXが長すぎないか
    • 画面をスクロールしないとコンポーネント全体が見えない場合、分割を検討する価値があります。
  • 別の画面でも使えそうか
    • 「この部分だけ別のページでも使えそうだな」と感じたら、コンポーネントとして切り出すチャンスです。

これらは「絶対ルール」ではありませんが、 迷ったときの設計の物差しとして役立ちます。

大きすぎるコンポーネントを見抜く「違和感センサー」を育てる

読んでいて「息切れする」コンポーネントは大きすぎることが多いです

大きすぎるコンポーネントを見抜く一番の方法は、

実際に読んでみて、「息切れするかどうか」を感じてみること

です。

例えば、次のような感覚があれば、大きすぎる可能性があります。

  • 読んでいる途中で「今何の処理を見ているのか」分からなくなる
  • スクロールしてもスクロールしても同じコンポーネントが続く
  • 入力フォームの処理を見ていたはずが、いつの間にか一覧表示の処理に入っている

こういうときは、責務を分けるチャンスです。

実際に「分割前」と「分割後」を比べてみる

たとえば、分割前の UserPage(全部入り版)と、 分割後の UserPage(親コンポーネント版)を並べてみると、違いがよく分かります。

分割前(全部入り)

  • 入力フォーム
  • バリデーション
  • 登録処理
  • 一覧表示
  • 削除処理

がすべて1つのコンポーネントに詰まっていて、 「どこからどこまでが何の責務か」が分かりづらいです。

分割後(親コンポーネント)

function UserPage() {
  const [users, setUsers] = useState([]);

  const handleAddUser = (user) => {
    setUsers((prevUsers) => [...prevUsers, user]);
  };

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

  return (
    <div>
      <h2>ユーザー登録ページ(分割版)</h2>

      <UserForm onAddUser={handleAddUser} />

      <div style={{ marginTop: "24px" }}>
        <UserList users={users} onDeleteUser={handleDeleteUser} />
      </div>
    </div>
  );
}
JSX

この状態だと、

  • UserPage は「ユーザー一覧の状態管理と、フォームと一覧の橋渡し」
  • 詳しい入力処理やバリデーションは UserForm に任せる
  • 詳しい表示や削除ボタンの配置は UserList / UserItem に任せる

という構造が一目で分かります。

重要なポイント:

「コンポーネントを開いた瞬間に、その責務が分かるかどうか」 これが、大きすぎるかどうかを判断するうえでの重要な感覚です。

小さすぎるコンポーネントを避ける「意味のある単位」の感覚

「意味のある単位」かどうかを自分に問いかける

小さすぎるコンポーネントを避けるためには、

「このコンポーネントは、意味のある単位を担当しているか?」

と自分に問いかけてみるとよいです。

例えば、

  • UserItem: 「1ユーザーの表示+削除ボタン」 → 意味のある単位です。
  • NameLabel: 「名前という文字だけを表示するラベル」 → 単体では意味が薄く、分割しすぎの可能性があります。

小さすぎるコンポーネントが増えると何が起きるか

コンポーネントを細かく分けすぎると、

  • ファイル数が増えすぎる
  • どこに何があるか分からなくなる
  • 1つ1つのコンポーネントの責務が薄くなる

という状態になります。

結果として、

  • 「コンポーネント設計がきれいなはずなのに、逆に読みにくい」
  • 「どこを直せばいいか分からない」

ということが起きてしまいます。

重要なポイント:

分割は「多ければ多いほど良い」わけではなく、 意味のある単位で分けることが大事です。

DAY 37 後半版テンプレート ― 設計判断のためのチェックリスト

最後に、DAY 37後半の内容を「設計判断のためのチェックリスト」としてまとめます。 コードを書く前・書いた後に、次のような問いを自分に投げかけてみてください。

コンポーネントの責務チェック

  • このコンポーネントは何をするものか、一文で説明できますか?
  • 説明が長くなりすぎていませんか?(複数の責務を抱えていませんか?)

分割する理由チェック

  • 読みやすさのために分割した方がよさそうですか?
  • 修正しやすさのために、責務を分けた方がよさそうですか?
  • 再利用できそうな部分は、コンポーネントとして切り出せそうですか?

大きすぎるコンポーネントチェック

  • Stateが多すぎませんか?
  • JSXが長すぎて、スクロールしないと全体が見えませんか?
  • 読んでいて「今何の処理を見ているのか」分からなくなる瞬間はありませんか?

小さすぎるコンポーネントチェック

  • そのコンポーネントは「意味のある単位」を担当していますか?
  • ラベルだけ・inputだけなど、あまりにも細かく分けすぎていませんか?
  • コンポーネントの数が増えすぎて、逆に管理しづらくなっていませんか?

DAY 37 後半のまとめ ― 「設計の目」を持ったReact開発者への一歩

DAY 37後半で身につけてほしいのは、次のような感覚です。

  • コンポーネントは「1コンポーネント=1責務」を基本に設計すること。
  • 分割は、「読みやすさ」「修正しやすさ」「再利用性」「テストのしやすさ」のために行うこと。
  • 大きすぎるコンポーネントは、読んだときの「息切れ感」や「説明の長さ」で気づけること。
  • 小さすぎるコンポーネントは、「意味のある単位かどうか」を問いかけることで避けられること。

この「設計の目」が育ってくると、 Reactでフォームを作るときも、画面を作るときも、 ただ動くコードではなく、長く付き合えるコードを書けるようになっていきます。

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