ToDoアプリ(State管理)解答例とポイント解説

2025-08-10

はじめに

この記事では、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の引数には状態の初期値を指定します(ここでは空の配列と空文字)
  • 状態を更新する際は必ず状態更新関数(setTodossetInputValue)を使用します

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の重要な概念:

  1. コンポーネントの状態管理useStateを使ったデータの保持と更新
  2. イベントハンドリング:ユーザー操作への応答方法
  3. リストレンダリング:配列データをJSXに変換する方法
  4. 条件付きレンダリング:状態に応じた表示の切り替え
  5. 不変性(Immutability):状態を直接変更せず、新しいオブジェクトを作成
  6. 双方向データバインディング:入力要素と状態の同期

よくある質問

Q: なぜ直接状態を変更してはいけないのですか?

A: Reactは状態の変更を検知して再レンダリングを行いますが、直接変更すると検知できない場合があります。また、不変性を保つことで予期しない副作用を防ぎ、デバッグが容易になります。

Q: keyプロパティになにを使うべきですか?

A: 理想的にはユニークで安定したID(データベースのIDなど)を使用します。この例では簡易的に配列のindexを使用していますが、リストが変更される可能性がある場合、indexを使うとパフォーマンス問題やバグの原因になることがあります。

Q: なぜスプレッド演算子(…)を使うのですか?

A: スプレッド演算子を使うことで、既存の配列やオブジェクトを浅くコピー(シャローコピー)できます。状態の不変性を保ちつつ、新しい配列/オブジェクトを作成するのに便利です。

まとめ

このToDoアプリの演習を通じて、Reactの状態管理の基本を実践的に学びました。小さなアプリですが、Reactの核心的な概念が詰まっています。次のステップとして、より複雑な状態管理(useReducerやContext API)や、コンポーネントの分割などに挑戦してみると良いでしょう。