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

React 60日で身につけるReact
スポンサーリンク
スポンサーリンク
  1. DAY 40:コンポーネント設計演習 前半 ― TODOアプリを4つのコンポーネントに分割する
  2. まずは「全部入り版」のTODOアプリを思い出す
    1. 1コンポーネントに全部詰め込んだ形をイメージする
  3. 分割の方針を決める:「何をどこまで担当させるか」
    1. Appの責務を最初に言葉で決める
    2. 子コンポーネントの責務をそれぞれ言葉にする
  4. Appコンポーネントを「状態のハブ」として書き直す
    1. AppはStateとロジックを持ち、子に渡すだけにする
  5. TodoForm:入力と「追加イベントの通知」に集中させる
    1. フォーム内部の入力値はフォーム自身が持つ
  6. TodoFilter:絞り込み条件のUIに責務を絞る
    1. 「今どのフィルターが選ばれているか」をPropsで受け取る
  7. TodoListとTodoItem:一覧表示と1件分の表示を分ける
    1. TodoListは「一覧のループ」と「イベントの受け渡し」を担当する
    2. TodoItemは「1件分の表示+完了切り替え+削除ボタン」を担当する
  8. DAY 40 前半のまとめ ― 「1コンポーネント=1責務」でTODOアプリを分割する
  9. DAY 40:コンポーネント設計演習 中盤 ― 「分割したあとの責務」と「Propsの流れ」を整理する
  10. コンポーネント分割後の全体像を「責務のマップ」として見る
    1. 全体構造をもう一度確認する
    2. 各コンポーネントの責務を一文で言えるか確認する
  11. Stateはどこに置くか ― 「Appに置くもの」と「子に閉じ込めるもの」
    1. Appが持つべきStateを整理する
    2. 子コンポーネントが持つべきStateを整理する
  12. Propsの流れを「データの矢印」として意識する
    1. Appから子へ「データ」と「イベントハンドラ」を渡す
    2. TodoListからTodoItemへ「1件分のデータ」と「イベント」を渡す
  13. 「責務の境界」がきれいかどうかをチェックする
    1. それぞれのコンポーネントが「やりすぎていないか」を見る
  14. 演習としてやってほしいこと ― 「自分で少し変えてみる」
    1. 1. TodoFormに「Enterキーで追加」を入れてみる
    2. 2. TodoFilterに「選択中のフィルターを強調表示」を入れてみる
  15. DAY 40 中盤のまとめ ― 「分割したあとを眺め直す目」を育てる
  16. DAY 40:コンポーネント設計演習 後半 ― 「設計を評価する目」と「改善する手」を育てる
  17. コンポーネント分割後のコードを「設計レビュー」してみる
    1. 全体構造をもう一度コードで確認する
  18. 責務の分割が「きれいかどうか」をチェックする
    1. Appの責務は「状態のハブ」として適切か
    2. 子コンポーネントが「やりすぎていないか」を見る
  19. Propsの流れが「分かりやすいかどうか」をチェックする
    1. 「誰が何を持っていて、誰に何を渡しているか」を言葉で説明する
    2. Props Drillingが起きていないかを確認する
  20. セキュリティと堅牢性の観点からも少しだけ見てみる
    1. 入力値の扱いとバリデーション
  21. 「もし機能を増やすならどうするか」を考えてみる
    1. 例1:期限(due date)を追加する場合
    2. 例2:完了済みTODOを一括削除するボタンを追加する場合
  22. DAY 40 後半のまとめ ― 「設計を評価し、少しずつ育てる」感覚を持つ

DAY 40:コンポーネント設計演習 前半 ― TODOアプリを4つのコンポーネントに分割する

DAY 40の前半では、DAY 30で作成したシンプルなTODOアプリを題材にして、 コンポーネント分割の実践演習を行っていきます。

目標の構成は次のとおりです。

  • App
    • TodoForm(入力フォーム)
    • TodoList(一覧表示)
      • TodoItem(1件分のTODO)
    • TodoFilter(絞り込み条件のUI)

ここでは、プログラミング初心者向けに、 「どこにStateを置くか」「どこまでを1コンポーネントの責務にするか」を意識しながら、 ステップバイステップで分割していきます。

まずは「全部入り版」のTODOアプリを思い出す

1コンポーネントに全部詰め込んだ形をイメージする

DAY 30では、おそらく次のような「全部入り版」のTODOアプリを書いていたはずです。

  • 入力フォーム(テキストボックス+追加ボタン)
  • TODO一覧表示
  • 完了/未完了の切り替え
  • 削除ボタン
  • 絞り込み(例:すべて/未完了/完了)

これらが、1つの App コンポーネントの中に詰め込まれていたイメージです。

ざっくりしたコード例は次のような感じです。

import { useState } from "react";

function App() {
  const [todos, setTodos] = useState([]);
  const [inputValue, setInputValue] = useState("");
  const [filter, setFilter] = useState("all"); // "all" | "active" | "completed"

  const handleAddTodo = () => {
    if (inputValue.trim() === "") return;

    const newTodo = {
      id: Date.now(),
      text: inputValue,
      completed: false,
    };

    setTodos((prev) => [...prev, newTodo]);
    setInputValue("");
  };

  const handleToggleTodo = (id) => {
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  const handleDeleteTodo = (id) => {
    setTodos((prev) => prev.filter((todo) => todo.id !== id));
  };

  const handleChangeFilter = (value) => {
    setFilter(value);
  };

  const filteredTodos = todos.filter((todo) => {
    if (filter === "active") return !todo.completed;
    if (filter === "completed") return todo.completed;
    return true;
  });

  return (
    <div>
      <h1>TODOアプリ</h1>

      {/* 入力フォーム */}
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="やることを入力してください"
      />
      <button onClick={handleAddTodo}>追加</button>

      {/* 絞り込み */}
      <div style={{ marginTop: "8px" }}>
        <button onClick={() => handleChangeFilter("all")}>すべて</button>
        <button onClick={() => handleChangeFilter("active")}>未完了</button>
        <button onClick={() => handleChangeFilter("completed")}>完了</button>
      </div>

      {/* TODO一覧 */}
      <ul style={{ marginTop: "16px" }}>
        {filteredTodos.map((todo) => (
          <li key={todo.id}>
            <label>
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => handleToggleTodo(todo.id)}
              />
              <span
                style={{
                  textDecoration: todo.completed ? "line-through" : "none",
                }}
              >
                {todo.text}
              </span>
            </label>
            <button
              style={{ marginLeft: "8px" }}
              onClick={() => handleDeleteTodo(todo.id)}
            >
              削除
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;
JSX

この「全部入り版」を、 責務ごとに4つのコンポーネントに分割していくのが、DAY 40前半の演習です。

分割の方針を決める:「何をどこまで担当させるか」

Appの責務を最初に言葉で決める

まずは、親コンポーネントである App の責務を言葉で整理します。

App の責務: 「TODO一覧の状態を管理し、 TodoForm・TodoList・TodoFilter をつなぐ親コンポーネント」

つまり、

  • todos(TODO一覧)と filter(絞り込み条件)のStateを持つ
  • 「追加」「完了切り替え」「削除」「絞り込み変更」のロジックを持つ
  • 子コンポーネントには「必要なデータ」と「必要なイベントハンドラ」をPropsとして渡す

という役割になります。

この「親の責務」を先に決めておくと、 子コンポーネントの責務がとても決めやすくなります。

子コンポーネントの責務をそれぞれ言葉にする

次に、分割後の各コンポーネントの責務を言葉で整理します。

  • TodoForm: 「新しいTODOを入力し、追加ボタンで親に知らせるフォーム」
  • TodoList: 「渡されたTODO一覧を表示し、各TODOの完了切り替え・削除イベントを親に知らせる」
  • TodoItem: 「1件分のTODOを表示し、完了切り替え・削除ボタンを持つ」
  • TodoFilter: 「絞り込み条件(すべて/未完了/完了)を選択するUIを提供し、変更を親に知らせる」

重要なポイント:

1コンポーネント=1責務 というイメージで、 「このコンポーネントは何をするものか?」を一文で説明できるようにしておきます。

Appコンポーネントを「状態のハブ」として書き直す

AppはStateとロジックを持ち、子に渡すだけにする

先ほどの「全部入り版」を、 分割後の構造を前提に書き直してみます。

import { useState } from "react";
import TodoForm from "./TodoForm";
import TodoList from "./TodoList";
import TodoFilter from "./TodoFilter";

function App() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState("all"); // "all" | "active" | "completed"

  const handleAddTodo = (text) => {
    if (text.trim() === "") return;

    const newTodo = {
      id: Date.now(),
      text,
      completed: false,
    };

    setTodos((prev) => [...prev, newTodo]);
  };

  const handleToggleTodo = (id) => {
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  const handleDeleteTodo = (id) => {
    setTodos((prev) => prev.filter((todo) => todo.id !== id));
  };

  const handleChangeFilter = (value) => {
    setFilter(value);
  };

  const filteredTodos = todos.filter((todo) => {
    if (filter === "active") return !todo.completed;
    if (filter === "completed") return todo.completed;
    return true;
  });

  return (
    <div>
      <h1>TODOアプリ</h1>

      <TodoForm onAddTodo={handleAddTodo} />

      <TodoFilter filter={filter} onChangeFilter={handleChangeFilter} />

      <TodoList
        todos={filteredTodos}
        onToggleTodo={handleToggleTodo}
        onDeleteTodo={handleDeleteTodo}
      />
    </div>
  );
}

export default App;
JSX

ここでのポイントは、

  • App は「状態のハブ」として、
    • todosfilter を持つ
    • 追加・切り替え・削除・絞り込み変更のロジックを持つ
  • 子コンポーネントは、
    • TodoForm:追加用の関数だけを受け取る
    • TodoFilter:現在の絞り込み条件と変更用の関数を受け取る
    • TodoList:絞り込み済みのTODO一覧と、切り替え・削除用の関数を受け取る

という役割分担になっていることです。

TodoForm:入力と「追加イベントの通知」に集中させる

フォーム内部の入力値はフォーム自身が持つ

TodoForm は、「新しいTODOを入力して、追加ボタンで親に知らせる」責務を持ちます。

import { useState } from "react";

function TodoForm({ onAddTodo }) {
  const [inputValue, setInputValue] = useState("");

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

    if (inputValue.trim() === "") {
      return;
    }

    onAddTodo(inputValue);
    setInputValue("");
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="やることを入力してください"
      />
      <button type="submit">追加</button>
    </form>
  );
}

export default TodoForm;
JSX

ここでの重要なポイントは、

  • 入力中のテキスト(inputValue)は、フォーム内部だけで意味を持つので、TodoForm がStateとして持つ
  • 新しいTODOを追加するロジック(IDを振る・配列に追加する)は、アプリ全体に関係するので、App が持つ
  • TodoForm は、「バリデーションを通過したテキストを onAddTodo(text) で親に渡すだけ」に責務を絞る

という役割分担になっていることです。

TodoFilter:絞り込み条件のUIに責務を絞る

「今どのフィルターが選ばれているか」をPropsで受け取る

TodoFilter は、「絞り込み条件を選ぶUI」を担当します。

function TodoFilter({ filter, onChangeFilter }) {
  return (
    <div style={{ marginTop: "8px" }}>
      <button
        onClick={() => onChangeFilter("all")}
        disabled={filter === "all"}
      >
        すべて
      </button>
      <button
        onClick={() => onChangeFilter("active")}
        disabled={filter === "active"}
        style={{ marginLeft: "4px" }}
      >
        未完了
      </button>
      <button
        onClick={() => onChangeFilter("completed")}
        disabled={filter === "completed"}
        style={{ marginLeft: "4px" }}
      >
        完了
      </button>
    </div>
  );
}

export default TodoFilter;
JSX

ここでのポイントは、

  • 現在の絞り込み条件(filter)は、TODO一覧の絞り込みに関係するので、App がStateとして持つ
  • TodoFilter は、「今どのフィルターが選ばれているか」をPropsとして受け取り、ボタンの disabled 状態などに反映する
  • フィルター変更のロジックは App が持ち、TodoFilteronChangeFilter("active") のように「イベントを知らせるだけ」にする

という役割分担です。

TodoListとTodoItem:一覧表示と1件分の表示を分ける

TodoListは「一覧のループ」と「イベントの受け渡し」を担当する

TodoList は、「渡されたTODO一覧をループして表示する」責務を持ちます。

import TodoItem from "./TodoItem";

function TodoList({ todos, onToggleTodo, onDeleteTodo }) {
  if (todos.length === 0) {
    return <p style={{ marginTop: "16px" }}>TODOはまだありません。</p>;
  }

  return (
    <ul style={{ marginTop: "16px", paddingLeft: "16px" }}>
      {todos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={() => onToggleTodo(todo.id)}
          onDelete={() => onDeleteTodo(todo.id)}
        />
      ))}
    </ul>
  );
}

export default TodoList;
JSX

ここでのポイントは、

  • TodoList は「一覧のループ」と「イベントの受け渡し」に責務を絞る
  • 実際の表示やチェックボックス・削除ボタンは、TodoItem に任せる

という構造にしていることです。

TodoItemは「1件分の表示+完了切り替え+削除ボタン」を担当する

TodoItem は、1件分のTODOの表示と、完了切り替え・削除ボタンを担当します。

function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <li style={{ marginBottom: "4px" }}>
      <label>
        <input
          type="checkbox"
          checked={todo.completed}
          onChange={onToggle}
        />
        <span
          style={{
            textDecoration: todo.completed ? "line-through" : "none",
            marginLeft: "4px",
          }}
        >
          {todo.text}
        </span>
      </label>
      <button
        style={{ marginLeft: "8px" }}
        onClick={onDelete}
      >
        削除
      </button>
    </li>
  );
}

export default TodoItem;
JSX

ここでの重要なポイントは、

  • TodoItem は、「1件分の表示」に責務を絞り、
    • todo(1件分のデータ)
    • onToggle(完了切り替えイベント)
    • onDelete(削除イベント) をPropsとして受け取る
  • 実際のState更新(完了フラグの切り替え・配列からの削除)は、App が担当する

という役割分担になっていることです。

DAY 40 前半のまとめ ― 「1コンポーネント=1責務」でTODOアプリを分割する

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

  • まず「全部入り版」のTODOアプリを思い出し、どの処理がどこに詰め込まれているかを整理すること。
  • 親コンポーネント App の責務を「TODO一覧と絞り込み条件のStateを持ち、子コンポーネントをつなぐ状態のハブ」として言葉で定義すること。
  • TodoFormTodoListTodoItemTodoFilter のそれぞれに、「1コンポーネント=1責務」のイメージで役割を割り当てること。
  • 入力中の値や1件分の表示など、「そのコンポーネントの中だけで意味を持つ状態」は子コンポーネントに持たせ、 TODO一覧や絞り込み条件など、「アプリ全体で意味を持つ状態」は親コンポーネントに持たせること。

この演習を通して、

「コンポーネントをどう分割するか」 「Stateをどこに置くか」

を自分で考えながら設計する感覚が、少しずつ育っていきます。


DAY 40:コンポーネント設計演習 中盤 ― 「分割したあとの責務」と「Propsの流れ」を整理する

DAY 40の中盤では、前半で分割した

  • App
  • TodoForm
  • TodoList
    • TodoItem
  • TodoFilter

という構成を、もう一段深く「設計の目」で眺め直す時間にしていきます。 単に「動くコード」を書くだけでなく、

「このコンポーネントは何を担当していて、どんなデータを受け取り、どんなイベントを親に返しているのか」

を言葉で説明できるようになることを目標にします。

コンポーネント分割後の全体像を「責務のマップ」として見る

全体構造をもう一度確認する

分割後の構造は次のようになっていました。

App
├─ TodoForm      (新規追加用フォーム)
├─ TodoFilter    (絞り込み条件のUI)
└─ TodoList      (一覧表示)
    └─ TodoItem  (1件分のTODO)

ここで重要なのは、

  • App が「状態のハブ」
  • 子コンポーネントは「表示+イベント通知」に集中

という役割分担になっていることです。

各コンポーネントの責務を一文で言えるか確認する

それぞれのコンポーネントを、一文で説明してみます。

  • App: TODO一覧と絞り込み条件のStateを持ち、 追加・切り替え・削除・絞り込み変更のロジックを管理し、 子コンポーネントをつなぐ親コンポーネントです。
  • TodoForm: 新しいTODOのテキストを入力し、 バリデーションを通過したテキストを親に渡すフォームコンポーネントです。
  • TodoFilter: 「すべて/未完了/完了」の絞り込み条件を選択するUIを提供し、 選択結果を親に知らせるコンポーネントです。
  • TodoList: 渡されたTODO一覧をループして表示し、 各TODOの完了切り替え・削除イベントを親に渡すコンポーネントです。
  • TodoItem: 1件分のTODOを表示し、 完了切り替えと削除ボタンを持つコンポーネントです。

重要なポイント:

「このコンポーネントは何をするものか?」を一文で説明できるかどうかが、 責務が適切に分割できているかを判断するひとつの目安になります。

Stateはどこに置くか ― 「Appに置くもの」と「子に閉じ込めるもの」

Appが持つべきStateを整理する

まず、App が持つべきStateを整理します。

function App() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState("all");
  // ...
}
JSX

App が持つStateは、

  • todos:TODO一覧(アプリ全体で意味を持つ状態)
  • filter:絞り込み条件(一覧表示に関係する状態)

です。

これらは、

  • TodoForm(追加)
  • TodoList(表示・切り替え・削除)
  • TodoFilter(絞り込み条件の変更)

と、複数のコンポーネントに関係するため、 共通の親である App に置くのが自然です。

子コンポーネントが持つべきStateを整理する

次に、子コンポーネント側のStateを見てみます。

TodoFormのState

function TodoForm({ onAddTodo }) {
  const [inputValue, setInputValue] = useState("");
  // ...
}
JSX

inputValue は、

  • 「フォームの中だけ」で意味を持つ
  • 親コンポーネントは、入力途中の値を知る必要がない

という性質を持つため、 TodoForm がStateとして持つのが適切です。

TodoList/TodoItem/TodoFilterのState

TodoListTodoItemTodoFilter は、 基本的に Stateを持たない構造にしていました。

  • TodoList
    • todos は親から渡される
    • 自分ではStateを持たず、「表示+イベントの受け渡し」に集中する
  • TodoItem
    • todo(1件分のデータ)は親から渡される
    • 完了切り替え・削除は、親から渡された関数を呼ぶだけ
  • TodoFilter
    • filter は親から渡される
    • 自分ではStateを持たず、「選択UI+イベント通知」に集中する

重要なポイント:

「そのコンポーネントの中だけで意味を持つ状態」は子に持たせ、 「複数コンポーネントに関係する状態」は親に持たせる。 これが、State配置の基本的な考え方です。

Propsの流れを「データの矢印」として意識する

Appから子へ「データ」と「イベントハンドラ」を渡す

App から子コンポーネントへのPropsの流れを整理してみます。

return (
  <div>
    <h1>TODOアプリ</h1>

    <TodoForm onAddTodo={handleAddTodo} />

    <TodoFilter filter={filter} onChangeFilter={handleChangeFilter} />

    <TodoList
      todos={filteredTodos}
      onToggleTodo={handleToggleTodo}
      onDeleteTodo={handleDeleteTodo}
    />
  </div>
);
JSX

ここで渡しているものは、

  • TodoForm
    • onAddTodo:新しいTODOを追加するための関数
  • TodoFilter
    • filter:現在の絞り込み条件
    • onChangeFilter:絞り込み条件を変更するための関数
  • TodoList
    • todos:絞り込み済みのTODO一覧
    • onToggleTodo:完了状態を切り替えるための関数
    • onDeleteTodo:TODOを削除するための関数

という形になっています。

TodoListからTodoItemへ「1件分のデータ」と「イベント」を渡す

TodoList から TodoItem へのPropsの流れも見てみます。

function TodoList({ todos, onToggleTodo, onDeleteTodo }) {
  return (
    <ul>
      {todos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={() => onToggleTodo(todo.id)}
          onDelete={() => onDeleteTodo(todo.id)}
        />
      ))}
    </ul>
  );
}
JSX

ここで渡しているものは、

  • todo:1件分のTODOデータ
  • onToggle:そのTODOの完了状態を切り替えるための関数
  • onDelete:そのTODOを削除するための関数

です。

TodoItem 側では、これをそのまま使います。

function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <li>
      <label>
        <input
          type="checkbox"
          checked={todo.completed}
          onChange={onToggle}
        />
        <span>{todo.text}</span>
      </label>
      <button onClick={onDelete}>削除</button>
    </li>
  );
}
JSX

重要なポイント:

Propsの流れは、「親から子へデータとイベントハンドラが流れ、 子から親へはイベント発火(関数呼び出し)で知らせる」という構造になっています。

「責務の境界」がきれいかどうかをチェックする

それぞれのコンポーネントが「やりすぎていないか」を見る

分割後のコードを眺めながら、 次のような観点で「責務の境界」がきれいかどうかをチェックしてみます。

TodoFormの責務チェック

  • 入力中の値(inputValue)を管理している
  • バリデーション(空文字チェック)をしている
  • バリデーションを通過したテキストを onAddTodo(text) で親に渡している

→ 「新規追加フォーム」としての責務にきれいに収まっています。

TodoFilterの責務チェック

  • 現在の絞り込み条件(filter)を受け取っている
  • ボタンの disabled 状態に反映している
  • ボタンが押されたときに onChangeFilter("active") などを呼んでいる

→ 「絞り込み条件のUI」としての責務にきれいに収まっています。

TodoList/TodoItemの責務チェック

  • TodoList は「一覧のループ」と「イベントの受け渡し」に集中している
  • TodoItem は「1件分の表示+完了切り替え+削除ボタン」に集中している
  • State更新のロジックはすべて App に集約されている

→ 「表示」と「状態更新」の責務がきれいに分かれています。

重要なポイント:

コンポーネントが「何でも屋」になっていないか、 「このコンポーネントは何を担当しているのか?」を自分に問いかける癖をつけると、 設計の質がぐっと上がります。

演習としてやってほしいこと ― 「自分で少し変えてみる」

1. TodoFormに「Enterキーで追加」を入れてみる

TodoForm に、Enterキーで追加できるような改善を入れてみると、 フォームの責務を意識しながらコードを触る練習になります。

すでに form タグを使っているので、 button type="submit"onSubmit の組み合わせで、 Enterキーでも追加できるようになっています。

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

  • 「フォームの改善は TodoForm の中だけで完結する」
  • 親コンポーネント App のコードには手を入れなくてよい

という点です。

2. TodoFilterに「選択中のフィルターを強調表示」を入れてみる

TodoFilter に、選択中のフィルターを強調表示する改善を入れてみるのもよい練習です。

例えば、選択中のボタンだけ背景色を変えるなどです。

function TodoFilter({ filter, onChangeFilter }) {
  const getStyle = (value) => ({
    marginLeft: value === "all" ? 0 : "4px",
    fontWeight: filter === value ? "bold" : "normal",
  });

  return (
    <div style={{ marginTop: "8px" }}>
      <button
        onClick={() => onChangeFilter("all")}
        style={getStyle("all")}
      >
        すべて
      </button>
      <button
        onClick={() => onChangeFilter("active")}
        style={getStyle("active")}
      >
        未完了
      </button>
      <button
        onClick={() => onChangeFilter("completed")}
        style={getStyle("completed")}
      >
        完了
      </button>
    </div>
  );
}
JSX

ここでも、

  • 見た目の改善は TodoFilter の中だけで完結する
  • 親コンポーネントの責務には影響しない

という構造になっていることが大事です。

DAY 40 中盤のまとめ ― 「分割したあとを眺め直す目」を育てる

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

  • 分割後の構造を、「責務のマップ」として眺め直し、 各コンポーネントが何を担当しているかを一文で説明できるようにすること。
  • Stateは、「そのコンポーネントの中だけで意味を持つものは子に」「複数コンポーネントに関係するものは親に」という基準で配置すること。
  • Propsの流れを、「親から子へデータとイベントハンドラが流れ、子から親へはイベント発火で知らせる」という矢印として意識すること。
  • コンポーネントが「何でも屋」になっていないか、「責務の境界」がきれいかどうかを自分でチェックする癖をつけること。
  • 小さな改善(Enterキー対応・選択中フィルターの強調など)を通じて、「どのコンポーネントを触るべきか」を意識しながらコードを変えてみること。

この「分割したあとを眺め直す目」が育ってくると、 Reactでアプリを作るときに、 ただ動くコードではなく、読みやすくて育てやすいコードを書けるようになっていきます。


DAY 40:コンポーネント設計演習 後半 ― 「設計を評価する目」と「改善する手」を育てる

DAY 40の後半では、DAY30のTODOアプリを

App
├─ TodoForm
├─ TodoList
│   └─ TodoItem
└─ TodoFilter

に分割したあとのコードを、「設計」という視点からもう一歩踏み込んで眺め直していきます。 ここでは、ただ分割できるようになるだけでなく、「この分割は本当に良いのか?」を自分で評価し、必要なら改善できるようになることを目標にします。

コンポーネント分割後のコードを「設計レビュー」してみる

全体構造をもう一度コードで確認する

まず、分割後の全体像をざっくりコードで振り返ります。

// App.jsx
import { useState } from "react";
import TodoForm from "./TodoForm";
import TodoList from "./TodoList";
import TodoFilter from "./TodoFilter";

function App() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState("all"); // "all" | "active" | "completed"

  const handleAddTodo = (text) => {
    if (text.trim() === "") return;

    const newTodo = {
      id: Date.now(),
      text,
      completed: false,
    };

    setTodos((prev) => [...prev, newTodo]);
  };

  const handleToggleTodo = (id) => {
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  const handleDeleteTodo = (id) => {
    setTodos((prev) => prev.filter((todo) => todo.id !== id));
  };

  const handleChangeFilter = (value) => {
    setFilter(value);
  };

  const filteredTodos = todos.filter((todo) => {
    if (filter === "active") return !todo.completed;
    if (filter === "completed") return todo.completed;
    return true;
  });

  return (
    <div>
      <h1>TODOアプリ</h1>

      <TodoForm onAddTodo={handleAddTodo} />

      <TodoFilter filter={filter} onChangeFilter={handleChangeFilter} />

      <TodoList
        todos={filteredTodos}
        onToggleTodo={handleToggleTodo}
        onDeleteTodo={handleDeleteTodo}
      />
    </div>
  );
}

export default App;
JSX

この App を中心に、 TodoFormTodoFilterTodoListTodoItem がぶら下がる構造になっています。

ここから、「どこが良くて、どこが改善余地なのか」を一緒に見ていきます。

責務の分割が「きれいかどうか」をチェックする

Appの責務は「状態のハブ」として適切か

App の責務は、

  • TODO一覧(todos)の管理
  • 絞り込み条件(filter)の管理
  • 追加・完了切り替え・削除・絞り込み変更のロジックの管理
  • 子コンポーネントへのデータとイベントハンドラの受け渡し

でした。

ここでチェックしたいのは、

  • 見た目の細かいこと(ボタンのスタイルなど)を App が持っていないか
  • 入力中の値や1件分の表示など、「子コンポーネントの中だけで意味を持つ状態」を App が抱えていないか

という点です。

今回の構成では、

  • 入力中の値は TodoForm が持っている
  • 1件分の表示は TodoItem が担当している
  • 絞り込みUIは TodoFilter が担当している

ので、App は「状態のハブ」として、かなりきれいな責務に収まっています。

子コンポーネントが「やりすぎていないか」を見る

それぞれの子コンポーネントについても、 「やりすぎていないか」をチェックしてみます。

TodoForm

function TodoForm({ onAddTodo }) {
  const [inputValue, setInputValue] = useState("");

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

    if (inputValue.trim() === "") {
      return;
    }

    onAddTodo(inputValue);
    setInputValue("");
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="やることを入力してください"
      />
      <button type="submit">追加</button>
    </form>
  );
}
JSX

責務は、

  • 入力値の管理
  • 簡単なバリデーション(空文字チェック)
  • 親への「追加イベント」の通知

に絞られており、 「TODO一覧の管理」や「絞り込み」などには一切関与していません。 これは、とても良い分割です。

TodoFilter

function TodoFilter({ filter, onChangeFilter }) {
  return (
    <div style={{ marginTop: "8px" }}>
      <button
        onClick={() => onChangeFilter("all")}
        disabled={filter === "all"}
      >
        すべて
      </button>
      <button
        onClick={() => onChangeFilter("active")}
        disabled={filter === "active"}
        style={{ marginLeft: "4px" }}
      >
        未完了
      </button>
      <button
        onClick={() => onChangeFilter("completed")}
        disabled={filter === "completed"}
        style={{ marginLeft: "4px" }}
      >
        完了
      </button>
    </div>
  );
}
JSX

責務は、

  • 現在の絞り込み条件の表示
  • 絞り込み条件変更のイベント通知

に絞られています。 TODO一覧のことは一切知らず、「フィルターUI」としての役割に集中できています。

TodoList/TodoItem

// TodoList.jsx
import TodoItem from "./TodoItem";

function TodoList({ todos, onToggleTodo, onDeleteTodo }) {
  if (todos.length === 0) {
    return <p style={{ marginTop: "16px" }}>TODOはまだありません。</p>;
  }

  return (
    <ul style={{ marginTop: "16px", paddingLeft: "16px" }}>
      {todos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={() => onToggleTodo(todo.id)}
          onDelete={() => onDeleteTodo(todo.id)}
        />
      ))}
    </ul>
  );
}

export default TodoList;
JSX
// TodoItem.jsx
function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <li style={{ marginBottom: "4px" }}>
      <label>
        <input
          type="checkbox"
          checked={todo.completed}
          onChange={onToggle}
        />
        <span
          style={{
            textDecoration: todo.completed ? "line-through" : "none",
            marginLeft: "4px",
          }}
        >
          {todo.text}
        </span>
      </label>
      <button
        style={{ marginLeft: "8px" }}
        onClick={onDelete}
      >
        削除
      </button>
    </li>
  );
}

export default TodoItem;
JSX

ここでの分割も、

  • TodoList:一覧のループ+イベントの受け渡し
  • TodoItem:1件分の表示+完了切り替え+削除ボタン

という役割分担になっており、 「1コンポーネント=1責務」の考え方に沿っています。

Propsの流れが「分かりやすいかどうか」をチェックする

「誰が何を持っていて、誰に何を渡しているか」を言葉で説明する

Propsの流れを、言葉で説明できるか確認してみます。

  • App は、
    • todosfilter を持ち、
    • TodoFormonAddTodo を渡し、
    • TodoFilterfilteronChangeFilter を渡し、
    • TodoListfilteredTodosonToggleTodoonDeleteTodo を渡しています。
  • TodoList は、
    • todos をループし、
    • TodoItemtodo と、 そのTODO専用の onToggleonDelete を渡しています。
  • TodoItem は、
    • 渡された todo を表示し、
    • チェックボックスや削除ボタンから onToggleonDelete を呼び出しています。

このように、

「誰が何を持っていて、誰に何を渡しているか」

を自然な日本語で説明できるなら、 Propsの流れはかなり分かりやすく設計できていると言えます。

Props Drillingが起きていないかを確認する

DAY 39で学んだ「Props Drilling」の観点からもチェックしてみます。

  • 同じPropsを、
    • App → TodoList → TodoItem のように複数階層で渡しているものはありますが、 それは「1件分のTODOを表示するために必要なデータ」であり、 間のコンポーネント(TodoList)もそのPropsを使っています。
  • 「まったく使っていないのに、次に渡すためだけに受け取っているProps」はありません。

つまり、

「通り道になっているだけのコンポーネント」は存在していない

状態になっており、 Props Drillingの悪いパターンにはなっていません。

セキュリティと堅牢性の観点からも少しだけ見てみる

入力値の扱いとバリデーション

TODOアプリはシンプルですが、 セキュリティや堅牢性の観点からも、少し意識しておくと良いポイントがあります。

  • TodoForm では、
    • 空文字を弾いている(trim() で空白だけの入力も防いでいる)
    • これは「最低限のバリデーション」として良い習慣です。
  • 実際のアプリでは、
    • 入力値をそのままHTMLに埋め込むとXSS(クロスサイトスクリプティング)の原因になりますが、
    • Reactは基本的にテキストをエスケープして表示するため、 通常の {todo.text} の表示であれば安全性は高いです。
  • ただし、
    • dangerouslySetInnerHTML を使う場合は、 入力値をそのままHTMLとして扱うことになるので、 必ずサニタイズ(危険なタグやスクリプトの除去)が必要になります。

今回のTODOアプリでは、 そういった危険な使い方はしていないため、 セキュリティ的にもシンプルで安全な構造になっています。

「もし機能を増やすならどうするか」を考えてみる

例1:期限(due date)を追加する場合

もしTODOに「期限(due date)」を追加したくなったとします。

このときの設計の考え方は次のようになります。

  • App
    • todos の1件分の構造に dueDate を追加する
    • 追加ロジック(handleAddTodo)で dueDate を含める
  • TodoForm
    • 期限入力用の <input type="date" /> を追加する
    • フォーム内部のStateとして dueDate を持つ
    • onAddTodo(text, dueDate) のように、親に渡す情報を増やす
  • TodoItem
    • todo.dueDate を表示する

このように、

「どのコンポーネントがその情報を本当に必要としているか」

を考えながら、 責務に沿って機能を追加していくことが大事です。

例2:完了済みTODOを一括削除するボタンを追加する場合

もうひとつ、 「完了済みTODOを一括削除する」ボタンを追加したくなったとします。

この場合、

  • App
    • handleClearCompleted を追加するjsxconst handleClearCompleted = () => { setTodos((prev) => prev.filter((todo) => !todo.completed)); };
    • そのボタンをどこに置くかを決める(例:TodoFilter の近く)
  • 新しいコンポーネントを作るかどうか:
    • ボタン1つだけなら、App に直接置いてもよい
    • 「完了済み管理エリア」のようなUIを作りたくなったら、 TodoActions のようなコンポーネントを切り出してもよい

ここでも、

「この機能はどのコンポーネントの責務に近いか?」

を考えながら、 追加先を決めていくのが設計の練習になります。

DAY 40 後半のまとめ ― 「設計を評価し、少しずつ育てる」感覚を持つ

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

  • コンポーネント分割後のコードを、「責務」「Stateの配置」「Propsの流れ」という観点から自分でレビューできること。
  • App は「状態のハブ」、子コンポーネントは「表示+イベント通知」に集中する構造になっているかを確認できること。
  • Props Drillingの悪いパターン(通り道だけのコンポーネント)が発生していないかを意識できること。
  • セキュリティや堅牢性の観点からも、入力値の扱いやバリデーションを軽くチェックできること。
  • 機能追加を考えたときに、「どのコンポーネントがその責務を持つべきか」を意識しながら、設計を少しずつ育てていけること。

この「設計を評価し、育てる目」が育ってくると、 Reactでアプリを作るときに、 ただ動くコードではなく、長く付き合えるコード・安心して機能追加できるコードを書けるようになっていきます。

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