【React v19入門】Todoアプリ作成から学び始める。3通りのコンポーネント構成方法とその特徴。
目次
初めてReact.jsを学ぼうとした時に、「何故コンポーネントをそんなに細かく分ける必要があるのか分からない」と思ったことはありませんか。「荒く分けると何が問題なのか」とか「どこまで分ければ良いのか」とか、「何を判断基準に分ければ良いのか」とか色々疑問が湧いてきます。
この記事は、馴染みのあるTodoアプリの作成を通して、Reactの概略を掴むためのものです。
用語説明
- JSX(JavaScript XML): HTMLのような構文でUIを記述できるJavaScriptの構文拡張機能です。JSX記法は中括弧{と }で囲んで使用します。
- Reactコンポーネント:画面に表示される部品。Reactコンポーネントには、関数コンポーネントとクラスコンポーネントがあります。ここでは関数コンポーネントを使って作成していきます。
フォルダ構成
npm create vite@latest で作成した時の最初のフォルダ構成

※)index.htmlからmain.jsxを読み込み、main.jsxからApp.jsxをimportして使っています。
コンポーネントとして関数を定義する場合は、「表示するエレメントをreturnで返す」ことが基本となります。App.jsxもそのような形になっていることが分かります。
Todosアプリで学習
ここでは簡単のためにTodosデータはApp.jsx側で配列として保持させます。
const [todos, setTodos] = useState([
{
id : 1,
name: "初めての投稿",
completed: false,
},{
id : 2,
name: "2回目の投稿",
completed: false,
}
])
構成1)App.jsxのみ使う
子コンポーネントを作らずにApp.jsxのみで実装した場合。ここでポイントとなる知識は、ulタグを使ったときのJSX記法で、JavaScriptの構文をどこに書くかです。

map関数で配列の各要素を1つずつ取り出し、コールバック関数で各要素の値を適当に使い、それらを纏めて一つのli要素として返すようにします。
見慣れないかもしれませんが、コード的にはtodos.map((todo)=>(...は、ulタグとliタグの間に書きます。
このように新たなコンポーネントを追加しなくても作成はできますが、Todosの配列データと表示が強く結びついているので、データの種類や処理が増えるとコードも複雑化して保守性や可読性が悪くなります。
構成2)TodoList.jsx(子コンポーネント)を一つ追加
リスト表示させる箇所を再利用可能なように子コンポーネントに分けます。TodoList.jsxというファイルをsrcフォルダに作成します。コンポーネント名の最初の一文字は必ず大文字にすることも忘れないでください。
コンポーネント間のデータの受け渡しにpropsを使用します。
親コンポーネントからtodosの名前で子コンポーネントのpropsに渡せば、子コンポーネントではprops.todosで受け取れます。あるいは、子コンポーネントが{todos}で受け取れば、そのままtodosとしてデータを使う事が出来ます。


機能追加して比較
TodoList.jsxの変更箇所
ここで、各Todoオブジェクトの削除機能と、completedの完了フラグをON/OFFする機能を定義します。この機能は親コンポーネント側で定義し、子コンポーネントに渡すものです。
App.jsx
/**タスクの削除 */
const handleDelete = (id) => {
if(!confirm('削除して良いですか')){
return;
}
const newTodos = todos.filter((todo) => {
return todo.id !== id;
});
setTodos(newTodos);
};
/**完了フラグの反転 */
const handleTodoToggle = (id) => {
const newTodos = [...todos];
const todo = newTodos.find((todo) => todo.id === id);
todo.completed = !todo.completed;
setTodos(newTodos);
};
return (
<>
<h2>Vite + React</h2>
<ul className='todo_ul'>
<TodoList
todos={todos}
TodoToggle={handleTodoToggle}
TodoDelete={handleDelete} />
</ul>
</>
]
配列データとイベント関数を受け取った子コンポーネント側は、todosの配列をmap()関数でtodo オブジェクトを1個ずつ取り出して処理していきます。個別の識別はtodo.idで行っていますが、コンポーネントのスコープ内でこの識別値が参照できるようにしないといけません。
TodoList.jsx
const TodoList = ({todos, TodoToggle, TodoDelete}) => {
return todos.map((todo)=>(
<li key={todo.id}>
<input type="checkbox"
checked={todo.completed}
// ここで個別のイベントハンドラーを設定する
onChange={() => TodoToggle(todo.id)} // アロー関数で TodoToggle を呼び出す
/>
<span>{todo.name}</span>
<span className="todoBtn"
onClick={()=>TodoDelete(todo.id)}>[削除]</span>
</li>
));
};
すなわち、return()で返す前に関数を、例えばhandleTodo(todo.id)のように定義しても、どのtodo.idか区別できませんのでhandleTodo(todo.id)が実行できません。従って、個別のtodo.idを引数に取ったイベントハンドラーをそのタグ内で直接書いて、親コンポーネントからのイベント関数を呼び出すようにしています。
全体と個別の両方を考えてコーディングしないといけないのが少し面倒な気もします。
構成3)TodoList.jsxを細分化してTodo.jsxを新規追加する
こちらが良く見かける方法になります。個々のtodo オブジェクトのことは、さらに細分化したコンポーネントに任せます。TodoList.jsxからtodoオブジェクトだけを扱えるTodo.jsxを切り出し、子コンポーネントとします。
Todo.jsx(最終的なコード)
const Todo = ({todo, TodoToggle, TodoDelete}) => {
// ここで親で定義した関数を使う
const handleToggle = () =>{
TodoToggle(todo.id);
};
// ここで親で定義した関数を使う
const handleDelete = () =>{
TodoDelete(todo.id);
}
return (
<li key={todo.id}>
<input type="checkbox"
checked={todo.completed}
onChange={handleToggle}
/>
<span>{todo.name}</span>
<span className="todoBtn" onClick={handleDelete}>[削除]</span>
</li>
)
}
export default Todo
例えば、onChange={handleToggle}ではcheckboxが変化すれば、このコンポーネントで定義したhandleToggle()関数を呼び出すようにして、handleToggle()関数では親が定義したTodoToggle()関数を使います。このコンポーネントでは個別のtodoオブジェクトしか扱わないため、識別値は正確に識別できて、TodoToggle(todo.id)が正しく実行されます。
親のTodoList.jsxでは、その親コンポーネントからの引数をバケツリレー式に渡せば良いだけですので、次のように変更します。
TodoList.jsx(最終コード)
TodoToggle={TodoToggle}は、TodoToggleという名前でそのままTodoToggle関数をTodoコンポーネントに渡します。
TodoDelete={TodoDelete}も同じで、ここでは何もせず、バケツリレーのように渡します。
App.jsx(最終コード)
import { useState, useRef } from 'react'
import './App.css'
import TodoList from './TodoList'
// import React from 'react' // ← <React.Fragment></React.Fragment> (<></>なら不要)
function App() {
const [todos, setTodos] = useState([
{
id : 1,
name: "初めての投稿",
completed: false,
},
{
id : 2,
name: "2回目の投稿",
completed: false,
}
]);
/**完了フラグの反転 */
const handleTodoToggle = (id) => {
const newTodos = [...todos];
const todo = newTodos.find((todo) => todo.id === id);
todo.completed = !todo.completed;
setTodos(newTodos);
};
const todoNameRef = useRef();
/**タスクの追加 */
const handleAddTodo = (e) => {
const name = todoNameRef.current.value;
if (name.length <4) {
todoNameRef.current.value = null;
return;
}
const befTodos = [...todos]
const maxId = befTodos.reduce((max, todo)=>Math.max(max, todo.id), 0)
const newItem = {
id : maxId +1,
name: name,
completed: false,
}
/**
* setTodos()は新しいステートの値を直接渡すことが多いと思いますが、
* このように関数(コールバック関数)を渡すこともできます。
* 関数を渡す場合、その関数の最初の引数には現在の(最新の)
* ステートの値が自動的に渡されます!!
**/
setTodos((prevTodos) => {
return [...prevTodos, newItem];
});
todoNameRef.current.value = null;
};
/**タスクの削除 */
const handleDelete = (id) => {
if(!confirm('削除して良いですか')){
return;
}
const newTodos = todos.filter((todo) => {
return todo.id !== id;
});
setTodos(newTodos);
};
return (
<>
<h2 className="mainTitle">投稿アプリ</h2>
<ul className='todo_ul'>
<TodoList todos={todos} TodoToggle={handleTodoToggle} TodoDelete={handleDelete} />
</ul>
<input type="text" className='todoInput' ref={todoNameRef} />
<button onClick={handleAddTodo}>投稿の追加</button>
</>
)
}
export default App
以上です😁