- DAY 40:コンポーネント設計演習 前半 ― TODOアプリを4つのコンポーネントに分割する
- まずは「全部入り版」のTODOアプリを思い出す
- 分割の方針を決める:「何をどこまで担当させるか」
- Appコンポーネントを「状態のハブ」として書き直す
- TodoForm:入力と「追加イベントの通知」に集中させる
- TodoFilter:絞り込み条件のUIに責務を絞る
- TodoListとTodoItem:一覧表示と1件分の表示を分ける
- DAY 40 前半のまとめ ― 「1コンポーネント=1責務」でTODOアプリを分割する
- DAY 40:コンポーネント設計演習 中盤 ― 「分割したあとの責務」と「Propsの流れ」を整理する
- コンポーネント分割後の全体像を「責務のマップ」として見る
- Stateはどこに置くか ― 「Appに置くもの」と「子に閉じ込めるもの」
- Propsの流れを「データの矢印」として意識する
- 「責務の境界」がきれいかどうかをチェックする
- 演習としてやってほしいこと ― 「自分で少し変えてみる」
- DAY 40 中盤のまとめ ― 「分割したあとを眺め直す目」を育てる
- DAY 40:コンポーネント設計演習 後半 ― 「設計を評価する目」と「改善する手」を育てる
- コンポーネント分割後のコードを「設計レビュー」してみる
- 責務の分割が「きれいかどうか」をチェックする
- Propsの流れが「分かりやすいかどうか」をチェックする
- セキュリティと堅牢性の観点からも少しだけ見てみる
- 「もし機能を増やすならどうするか」を考えてみる
- DAY 40 後半のまとめ ― 「設計を評価し、少しずつ育てる」感覚を持つ
DAY 40:コンポーネント設計演習 前半 ― TODOアプリを4つのコンポーネントに分割する
DAY 40の前半では、DAY 30で作成したシンプルなTODOアプリを題材にして、 コンポーネント分割の実践演習を行っていきます。
目標の構成は次のとおりです。
AppTodoForm(入力フォーム)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は「状態のハブ」として、todosとfilterを持つ- 追加・切り替え・削除・絞り込み変更のロジックを持つ
- 子コンポーネントは、
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が持ち、TodoFilterはonChangeFilter("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を持ち、子コンポーネントをつなぐ状態のハブ」として言葉で定義すること。 TodoForm・TodoList・TodoItem・TodoFilterのそれぞれに、「1コンポーネント=1責務」のイメージで役割を割り当てること。- 入力中の値や1件分の表示など、「そのコンポーネントの中だけで意味を持つ状態」は子コンポーネントに持たせ、 TODO一覧や絞り込み条件など、「アプリ全体で意味を持つ状態」は親コンポーネントに持たせること。
この演習を通して、
「コンポーネントをどう分割するか」 「Stateをどこに置くか」
を自分で考えながら設計する感覚が、少しずつ育っていきます。
DAY 40:コンポーネント設計演習 中盤 ― 「分割したあとの責務」と「Propsの流れ」を整理する
DAY 40の中盤では、前半で分割した
AppTodoFormTodoListTodoItem
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");
// ...
}
JSXApp が持つStateは、
todos:TODO一覧(アプリ全体で意味を持つ状態)filter:絞り込み条件(一覧表示に関係する状態)
です。
これらは、
TodoForm(追加)TodoList(表示・切り替え・削除)TodoFilter(絞り込み条件の変更)
と、複数のコンポーネントに関係するため、 共通の親である App に置くのが自然です。
子コンポーネントが持つべきStateを整理する
次に、子コンポーネント側のStateを見てみます。
TodoFormのState
function TodoForm({ onAddTodo }) {
const [inputValue, setInputValue] = useState("");
// ...
}
JSXinputValue は、
- 「フォームの中だけ」で意味を持つ
- 親コンポーネントは、入力途中の値を知る必要がない
という性質を持つため、 TodoForm がStateとして持つのが適切です。
TodoList/TodoItem/TodoFilterのState
TodoList・TodoItem・TodoFilter は、 基本的に 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 を中心に、 TodoForm・TodoFilter・TodoList・TodoItem がぶら下がる構造になっています。
ここから、「どこが良くて、どこが改善余地なのか」を一緒に見ていきます。
責務の分割が「きれいかどうか」をチェックする
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は、todosとfilterを持ち、TodoFormにonAddTodoを渡し、TodoFilterにfilterとonChangeFilterを渡し、TodoListにfilteredTodosとonToggleTodo・onDeleteTodoを渡しています。
TodoListは、todosをループし、- 各
TodoItemにtodoと、 そのTODO専用のonToggle・onDeleteを渡しています。
TodoItemは、- 渡された
todoを表示し、 - チェックボックスや削除ボタンから
onToggle・onDeleteを呼び出しています。
- 渡された
このように、
「誰が何を持っていて、誰に何を渡しているか」
を自然な日本語で説明できるなら、 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のようなコンポーネントを切り出してもよい
- ボタン1つだけなら、
ここでも、
「この機能はどのコンポーネントの責務に近いか?」
を考えながら、 追加先を決めていくのが設計の練習になります。
DAY 40 後半のまとめ ― 「設計を評価し、少しずつ育てる」感覚を持つ
DAY 40後半で身につけてほしいのは、次のような感覚です。
- コンポーネント分割後のコードを、「責務」「Stateの配置」「Propsの流れ」という観点から自分でレビューできること。
Appは「状態のハブ」、子コンポーネントは「表示+イベント通知」に集中する構造になっているかを確認できること。- Props Drillingの悪いパターン(通り道だけのコンポーネント)が発生していないかを意識できること。
- セキュリティや堅牢性の観点からも、入力値の扱いやバリデーションを軽くチェックできること。
- 機能追加を考えたときに、「どのコンポーネントがその責務を持つべきか」を意識しながら、設計を少しずつ育てていけること。
この「設計を評価し、育てる目」が育ってくると、 Reactでアプリを作るときに、 ただ動くコードではなく、長く付き合えるコード・安心して機能追加できるコードを書けるようになっていきます。

