Reactの学習を進めていくと、「そろそろ何か作ってみたい」と思うタイミングが来ます。そんなときの最初の制作物としておすすめなのがTODOアプリです。
TODOアプリは、Reactの基本的な機能を一通り使う必要があるため、学んだ知識を実践に落とし込む良い練習になります。
この記事では、ReactのuseStateを使ったシンプルなTODOアプリの作り方を解説します。

対象読者
- Reactの基礎(JSX、コンポーネント、Props、State)を学習済みの方
- フォーム操作の基本を理解している方
- 初めてReactで何か作ってみたい方
TODOアプリに必要な3つの操作
TODOアプリは、突き詰めると以下の3つの操作で成り立っています。
- 追加する — 配列に新しいタスクを足す
- 削除する — 配列からタスクを取り除く
- 状態を変える — タスクの完了/未完了を切り替える
これらはすべて、useStateと配列メソッド(map、filter)で実現できます。
完成コード
todo.jsx
import { useState } from "react";
import "./todo.css";
export default function StateTodo() {
// Todo項目idの最大値を管理するためのstate
const [maxId, setMaxId] = useState(1);
// Todo項目のリストを管理するためのstate
const [title, setTitle] = useState("");
const [todos, setTodos] = useState([]);
// テキストボックスへの入力値をstateに反映
const handleChangeTitle = (e) => {
setTitle(e.target.value);
};
// Todoを追加する関数
const handleClickAdd = () => {
// 入力値が空の場合は追加しない
if (title === "") {
alert("やることを入力してください");
return;
}
// 新しいTodo項目を追加
setTodos([
...todos,
{
id: maxId,
title: title,
created: new Date(),
isDone: false,
},
]);
// Todo項目idの最大値を更新
setMaxId((id) => id + 1);
// テキストボックスを空にする
setTitle("");
};
// Todo項目の済み/未済みを切り替える関数
const handleDone = (e) => {
setTodos(
todos.map((item) => {
if (item.id === Number(e.target.dataset.id)) {
return {
...item,
isDone: !item.isDone,
};
} else {
return item;
}
})
);
};
// Todoを削除する関数
const handleRemove = (e) => {
setTodos(todos.filter((item) => item.id !== Number(e.target.dataset.id)));
};
// 表示をする
return (
<div>
<label>
やること:
<input
type="text"
name="title"
value={title}
className="title-input"
onChange={handleChangeTitle}
/>
</label>
<button type="button" className="add-button" onClick={handleClickAdd}>
追加
</button>
<hr />
<ul>
{todos.map((item) => (
<li key={item.id} className={`todo ${item.isDone ? "done" : ""}`}>
{item.title}
<button
className={`todo-button ${item.isDone ? "done-button" : "not-done-button"}`}
type="button"
onClick={handleDone}
data-id={item.id}
>
{item.isDone ? "完了" : "未完了"}
</button>
<button
className="remove-button"
type="button"
onClick={handleRemove}
data-id={item.id}
>
削除
</button>
</li>
))}
</ul>
</div>
);
}
todo.css
.done {
text-decoration: line-through;
}
.add-button {
margin-left: 10px;
height: auto;
}
.title-input {
width: 200px;
height: 30px;
}
.todo {
list-style: none;
border: 1px solid black;
border-radius: 10px;
padding: 10px;
margin: 10px;
background-color: #f0f0f0;
}
.done-button {
background-color: lightgreen;
}
.not-done-button {
background-color: khaki;
}
.todo-button {
display: inline-block;
margin-left: 10px;
}
.remove-button {
display: inline-block;
margin-left: 10px;
background-color: lightcoral;
}
コード解説
Stateの設計
const [maxId, setMaxId] = useState(1);
const [title, setTitle] = useState("");
const [todos, setTodos] = useState([]);
このアプリでは3つのStateを使います。
maxId: 各タスクに一意のIDを振るためのカウンター。タスクが追加されるたびに1ずつ増えます。title: 入力フォームの値。ユーザーが入力した文字をリアルタイムで保持します。todos: タスクの配列。各タスクは{ id, title, created, isDone }というオブジェクトの形を持ちます。
タスクの追加 — スプレッド構文と早期リターン
const handleClickAdd = () => {
if (title === "") {
alert("やることを入力してください");
return;
}
setTodos([
...todos,
{
id: maxId,
title: title,
created: new Date(),
isDone: false,
},
]);
setMaxId((id) => id + 1);
setTitle("");
};
ポイントは2つあります。
早期リターン(early return): 関数の先頭でtitleが空かどうかをチェックし、空ならalertを出してreturnで処理を終了します。こうすることでelseを書く必要がなくなり、コードの見通しが良くなります。
スプレッド構文(...todos): Reactでは、Stateの配列を直接変更(ミューテーション)してはいけません。...todosで既存の配列を展開し、末尾に新しいタスクを追加した新しい配列を作成しています。
追加後はsetTitle("")で入力欄を空にしています。これにより、ユーザーは続けて次のタスクを入力できます。
完了/未完了の切り替え — mapと論理否定
const handleDone = (e) => {
setTodos(
todos.map((item) => {
if (item.id === Number(e.target.dataset.id)) {
return {
...item,
isDone: !item.isDone,
};
} else {
return item;
}
})
);
};
mapメソッドで配列の全要素をループし、クリックされたボタンのdata-idと一致するタスクだけisDoneを反転させます。!item.isDoneとすることで、trueならfalseに、falseならtrueに切り替わります。
一致しないタスクはそのまま返すことで、他のタスクに影響を与えません。
タスクの削除 — filter
const handleRemove = (e) => {
setTodos(todos.filter((item) => item.id !== Number(e.target.dataset.id)));
};
filterメソッドは、条件に一致する要素だけを残した新しい配列を返します。ここでは「クリックされたIDと一致しないもの」だけを残すことで、該当のタスクを削除しています。
JSXでの条件分岐 — テンプレートリテラルと三項演算子
<li key={item.id} className={`todo ${item.isDone ? "done" : ""}`}>
テンプレートリテラル(` `)を使って、複数のCSSクラスを組み合わせています。todoクラスは常に適用され、doneクラスは完了時のみ追加されます。
この書き方のメリットは、CSSの重複を防げることです。カードの見た目(border、border-radius、paddingなど)は.todoに一度だけ書き、完了時の装飾(text-decoration: line-through)は.doneに分離しています。
ボタンのテキストにも同じパターンを使っています。
{item.isDone ? "完了" : "未完了"}
Reactでは、JSXの{}の中でif文を直接書くことができないため、シンプルな二択の場合は三項演算子がよく使われます。
まとめ
TODOアプリは「追加」「削除」「状態の切り替え」という3つの操作で構成されており、それぞれuseState、map、filterというReactとJavaScriptの基本機能で実現できます。
この記事で扱った主なテクニックは以下のとおりです。
useStateによる状態管理- スプレッド構文による配列の非破壊的な更新
mapによるリスト表示と要素の更新filterによる要素の削除- 早期リターンによるバリデーション
- テンプレートリテラルと三項演算子によるクラスの動的切り替え
ここから先は、Flexboxを使ったレイアウトの改善、登録日時の表示、ローカルストレージへの保存など、さまざまな拡張が考えられます。まずは動くものを作り、そこから少しずつ機能を追加していくのが、React学習の良いサイクルです。


コメント