はじめに
この記事では、React初学者向けのToDoアプリ演習の解答例と、この演習で学ぶ重要なポイントについて詳しく解説します。State管理というReactの核心的な概念を、実際のコードを通じて理解していきましょう。
解答例コード
まずは完成形のコード全体を見てみましょう。src/App.jsの内容です:
import React, { useState } from 'react';
import './App.css';
function App() {
// Stateの宣言
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
// ToDoを追加する関数
const addTodo = () => {
if (inputValue.trim()) { // 空文字やスペースのみの入力を防ぐ
setTodos([...todos, { text: inputValue, completed: false }]);
setInputValue(''); // 入力欄を空にする
}
};
// ToDoの完了/未完了を切り替える関数
const toggleTodo = (index) => {
const newTodos = [...todos]; // 現在のtodos配列をコピー
newTodos[index].completed = !newTodos[index].completed; // 指定したToDoの状態を反転
setTodos(newTodos); // 更新した配列でStateを更新
};
return (
<div className="app">
<h1>ToDoアプリ</h1>
{/* ToDo追加フォーム */}
<div className="todo-form">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="新しいToDoを入力"
onKeyPress={(e) => e.key === 'Enter' && addTodo()} // Enterキーでも追加可能
/>
<button onClick={addTodo}>追加</button>
</div>
{/* ToDoリスト表示 */}
<ul className="todo-list">
{todos.map((todo, index) => (
<li
key={index}
onClick={() => toggleTodo(index)}
className={todo.completed ? 'completed' : ''}
>
<span>{todo.text}</span>
<button
className="delete-btn"
onClick={(e) => {
e.stopPropagation(); // 親要素へのイベント伝播を防ぐ
const newTodos = todos.filter((_, i) => i !== index);
setTodos(newTodos);
}}
>
削除
</button>
</li>
))}
</ul>
{/* 完了済みToDoカウンター */}
<div className="counter">
完了: {todos.filter(todo => todo.completed).length} / 全体: {todos.length}
</div>
</div>
);
}
export default App;
重要なポイント解説
1. useStateフックの基本
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
useStateはReactのフックの1つで、コンポーネント内で状態を管理するために使用します- 配列の分割代入を使って、
状態変数と状態更新関数を取得します useStateの引数には状態の初期値を指定します(ここでは空の配列と空文字)- 状態を更新する際は必ず状態更新関数(
setTodosやsetInputValue)を使用します
2. ToDo追加の処理
const addTodo = () => {
if (inputValue.trim()) {
setTodos([...todos, { text: inputValue, completed: false }]);
setInputValue('');
}
};
inputValue.trim()で入力値の前後の空白を削除し、空でないことを確認- スプレッド演算子(
...)を使って既存のtodosを展開し、新しいToDoを追加 - 新しいToDoはオブジェクト形式で、
text(内容)とcompleted(完了状態)のプロパティを持つ - 追加後は
setInputValue('')で入力欄をクリア
3. 状態の不変性(Immutability)
const toggleTodo = (index) => {
const newTodos = [...todos]; // 配列をコピー
newTodos[index].completed = !newTodos[index].completed;
setTodos(newTodos);
};
- Reactでは状態を直接変更せず、新しいオブジェクト/配列を作成して更新します
[...todos]で配列のシャローコピーを作成- コピーした配列の該当要素を更新し、
setTodosで状態を更新 - これによりReactは変更を検知し、再レンダリングが発生します
4. リストのレンダリング
<ul className="todo-list">
{todos.map((todo, index) => (
<li key={index} onClick={() => toggleTodo(index)}>
{/* ... */}
</li>
))}
</ul>
mapメソッドを使って配列をJSX要素に変換- 各リストアイテムには一意の
keyプロパティが必要(ここではindexを使用) keyはReactが要素を効率的に更新するために使用されます
5. イベントハンドリング
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
- Reactではイベントハンドラに関数を直接渡します
onChangeで入力値の変更を検知し、setInputValueで状態を更新- これにより入力欄とReactの状態が同期(双方向バインディング)
6. 条件付きスタイリング
<li className={todo.completed ? 'completed' : ''}>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
</li>
- 三項演算子を使って条件に応じたクラス名やスタイルを適用
- 完了済みToDoには打ち消し線を表示
発展的な機能の実装
解答例には以下の発展的な機能も含めています:
1. Enterキーでの追加
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
- 入力欄でEnterキーを押したときにもToDoを追加できるように
2. ToDo削除機能
onClick={(e) => {
e.stopPropagation();
const newTodos = todos.filter((_, i) => i !== index);
setTodos(newTodos);
}}
filterメソッドで該当するToDoを除いた新しい配列を作成e.stopPropagation()で親要素のクリックイベントが発火しないように防止
3. 進捗カウンター
<div className="counter">
完了: {todos.filter(todo => todo.completed).length} / 全体: {todos.length}
</div>
filterメソッドで完了済みToDoを抽出し、その数を表示
CSSスタイルの例
src/App.cssの内容例:
.app {
max-width: 500px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.todo-form {
display: flex;
margin-bottom: 20px;
}
.todo-form input {
flex-grow: 1;
padding: 10px;
font-size: 16px;
border: 1px solid #ddd;
border-radius: 4px;
}
.todo-form button {
padding: 10px 15px;
margin-left: 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
margin-bottom: 8px;
background-color: #f9f9f9;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.todo-list li:hover {
background-color: #f0f0f0;
}
.todo-list li.completed {
background-color: #e8f5e9;
}
.todo-list li.completed span {
text-decoration: line-through;
color: #888;
}
.delete-btn {
padding: 5px 10px;
background-color: #ff4444;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
.delete-btn:hover {
background-color: #cc0000;
}
.counter {
margin-top: 20px;
text-align: right;
color: #666;
font-size: 14px;
}
学ぶべき重要な概念
このToDoアプリを通じて学ぶべきReactの重要な概念:
- コンポーネントの状態管理:
useStateを使ったデータの保持と更新 - イベントハンドリング:ユーザー操作への応答方法
- リストレンダリング:配列データをJSXに変換する方法
- 条件付きレンダリング:状態に応じた表示の切り替え
- 不変性(Immutability):状態を直接変更せず、新しいオブジェクトを作成
- 双方向データバインディング:入力要素と状態の同期
よくある質問
Q: なぜ直接状態を変更してはいけないのですか?
A: Reactは状態の変更を検知して再レンダリングを行いますが、直接変更すると検知できない場合があります。また、不変性を保つことで予期しない副作用を防ぎ、デバッグが容易になります。
Q: keyプロパティになにを使うべきですか?
A: 理想的にはユニークで安定したID(データベースのIDなど)を使用します。この例では簡易的に配列のindexを使用していますが、リストが変更される可能性がある場合、indexを使うとパフォーマンス問題やバグの原因になることがあります。
Q: なぜスプレッド演算子(…)を使うのですか?
A: スプレッド演算子を使うことで、既存の配列やオブジェクトを浅くコピー(シャローコピー)できます。状態の不変性を保ちつつ、新しい配列/オブジェクトを作成するのに便利です。
まとめ
このToDoアプリの演習を通じて、Reactの状態管理の基本を実践的に学びました。小さなアプリですが、Reactの核心的な概念が詰まっています。次のステップとして、より複雑な状態管理(useReducerやContext API)や、コンポーネントの分割などに挑戦してみると良いでしょう。