Reactアプリ開発！　ToDoアプリを作りながら学ぶフロントエンドの基礎



## はじめに

この記事では、比較的シンプルながらWebアプリケーションの基本的な要素がたくさん詰まった**ToDoアプリ**を、Reactを使ってゼロから一緒に作っていきます。

**なぜToDoアプリ？**

* **基本が学べる:** リスト表示、データの追加・変更・削除といった、多くのアプリで使われる基本操作を実装します。

**この記事で作るToDoアプリの機能:**

1.  新しいToDoをテキスト入力して追加できる。
2.  追加されたToDoがリスト表示される。
3.  ToDoをクリックすると完了/未完了状態を切り替えられる（見た目が変わる）。
4.  不要なToDoを削除できる。



## ステップ1: Reactプロジェクトの準備

まずは、Reactプロジェクトの土台を作ります。最近主流の**Vite**というツールを使うと、簡単に高速な開発環境を準備できます。

ターミナル（WindowsならコマンドプロンプトやPowerShell、Macならターミナル）を開いて、以下のコマンドを実行してください。`my-todo-app` は好きな名前に変えてOKです。

```bash
npm create vite@latest my-todo-app --template react-ts
```

いくつか質問されますが、基本的にはEnterキーを押していけばOKです。（もし`Need to install the following packages: create-vite@x.x.x Ok to proceed? (y)` と聞かれたら `y` を入力してEnter）

次に、作成したプロジェクトのフォルダに移動し、必要なプログラム（ライブラリ）をインストールします。

```bash
cd my-todo-app
npm install
```

インストールが終わったら、開発用のサーバーを起動します。

```bash
npm run dev
```

ターミナルに `Local: http://localhost:xxxx` のようなURLが表示されるので、そのURLをWebブラウザで開いてください。Reactのサンプルページが表示されれば成功です！

**少しだけコードを整理しよう:**

最初から入っているサンプルコードを少し整理して、開発を始めやすくしましょう。

1.  **`src/App.css`**: ファイルの中身をすべて削除して空にします。（後で自分でスタイルを書きます）
2.  **`src/index.css`**: こちらも一旦中身をすべて削除してOKです。（必要なら後で基本的なスタイルを追加します）
3.  **`src/App.tsx`**: このファイルがメインの画面を作る場所になります。中身を以下のようにシンプルに書き換えてください。

    ```typescript
    // src/App.tsx
    import React from 'react'; // Reactを使うためのおまじない
    import './App.css'; // CSSファイルを読み込み

    function App() {
      // このreturn()の中に画面に表示したい内容を書いていく
      return (
        <div className="App">
          <h1>ToDoアプリ</h1>
          {/* ここにToDoアプリの要素を追加していく */}
        </div>
      );
    }

    export default App; // 他のファイルで使えるようにするおまじない
    ```

ブラウザの表示が「ToDoアプリ」というタイトルだけのシンプルなものに変わればOKです。

## ステップ2: UIの骨組みを作る (JSX)

まずは、ToDoアプリに必要な見た目の部品をHTMLのような形式で配置していきましょう。Reactではこれを**JSX**という記法で書きます。

`src/App.tsx` を編集して、タイトル、ToDoを入力するフォーム、ToDoリストを表示するエリアの骨組みを作ります。

```typescript
// src/App.tsx
import React from 'react';
import './App.css';

function App() {
  return (
    <div className="App">
      <h1>ToDoアプリ</h1>

      {/* ToDo入力フォーム */}
      <form>
        <input type="text" placeholder="新しいToDoを入力" />
        <button type="submit">追加</button>
      </form>

      {/* ToDoリスト表示エリア */}
      <h2>未完了のToDo</h2>
      <ul>
        {/* ここに未完了ToDoリストが表示される予定 */}
        <li>サンプルToDo 1 (未完了)</li>
      </ul>

      <h2>完了したToDo</h2>
      <ul>
        {/* ここに完了ToDoリストが表示される予定 */}
        <li>サンプルToDo 2 (完了)</li>
      </ul>
    </div>
  );
}

export default App;
```

**少しだけスタイルを整えよう (任意):**

このままだと見た目が寂しいので、`src/index.css` に少しだけCSSを追加して最低限の見た目を整えましょう。（CSSに慣れていない方は飛ばしてもOKです）

```css
/* src/index.css */
body {
  font-family: sans-serif;
  margin: 20px;
  background-color: #f4f4f4;
}

.App {
  max-width: 600px;
  margin: 0 auto;
  background-color: #fff;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

h1, h2 {
  text-align: center;
  color: #333;
}

form {
  display: flex;
  margin-bottom: 20px;
}

input[type="text"] {
  flex-grow: 1;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  margin-right: 10px;
}

button {
  padding: 10px 15px;
  background-color: #5cb85c;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  background-color: #4cae4c;
}

ul {
  list-style: none;
  padding: 0;
}

li {
  background-color: #eee;
  margin-bottom: 10px;
  padding: 10px;
  border-radius: 4px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

li button {
  background-color: #d9534f;
  padding: 5px 10px;
  font-size: 0.9em;
}

li button:hover {
  background-color: #c9302c;
}

/* 完了したToDo用のスタイル (後で使う) */
.completed {
  text-decoration: line-through;
  color: #777;
  background-color: #e0e0e0;
}
```

ブラウザで確認し、入力フォームとサンプルのリストが表示されていればOKです。

## ステップ3: ToDoリストを配列で表示する

今はサンプルToDoが直接書かれていますが、これをプログラムで扱えるように、JavaScriptの**配列**データからリスト表示するように変更します。

まず、`App` 関数の**中**（`return` の前）に、サンプルのToDoデータを持つ配列を定義します。各ToDoは、内容を示す `text` と、完了したかどうかを示す `completed`、そして後で個々を区別するための `id` を持つオブジェクトとします。

```typescript
// src/App.tsx
import React from 'react';
import './App.css';

// ToDoアイテムの型を定義しておくと便利 (TypeScript)
type Todo = {
  id: number;
  text: string;
  completed: boolean;
};

function App() {
  // サンプルのToDoリスト配列
  const initialTodos: Todo[] = [
    { id: 1, text: 'Reactの基本を学ぶ', completed: false },
    { id: 2, text: 'ToDoアプリを作る', completed: false },
    { id: 3, text: '休憩する', completed: true }, // 1つ完了済みにしてみる
  ];

  // 配列の map メソッドを使って、各ToDoを<li>要素に変換する
  const todoListItems = initialTodos.map(todo => (
    // ★★★ key プロパティを必ず指定する ★★★
    // Reactがリストの変更を効率的に追跡するために必要
    <li key={todo.id}>
      {todo.text}
    </li>
  ));

  return (
    <div className="App">
      <h1>ToDoアプリ</h1>

      {/* ToDo入力フォーム (変更なし) */}
      <form>
        <input type="text" placeholder="新しいToDoを入力" />
        <button type="submit">追加</button>
      </form>

      {/* ToDoリスト表示エリア */}
      <h2>ToDoリスト</h2>
      <ul>
        {/* 配列から生成したリスト項目を表示 */}
        {todoListItems}
      </ul>

      {/* 完了/未完了の表示分けは後でやるので一旦削除 */}
      {/*
      <h2>未完了のToDo</h2> ...
      <h2>完了したToDo</h2> ...
      */}
    </div>
  );
}

export default App;
```

**ポイント:**

* `initialTodos.map(todo => ...)`: 配列の `map` メソッドは、配列の各要素に対して指定した処理を行い、その結果から新しい配列を作るJavaScriptの機能です。ここでは、各 `todo` オブジェクトを `<li>` 要素に変換しています。
* `key={todo.id}`: `map` でリストを生成する場合、各要素には**ユニークな `key` プロパティ**を指定する必要があります。これはReactがリスト項目を区別するために使います。ここでは各ToDoが持つ `id` を使っています。 **`key` を指定しないとエラーが出たり、予期せぬ動作の原因になるので非常に重要です。**

ブラウザで確認し、配列の内容がリスト表示されていれば成功です。

## ステップ4: 新しいToDoを追加する機能 (Stateとフォーム)

いよいよアプリに動きをつけていきます！入力フォームにToDoを書いて「追加」ボタンを押したら、それがリストに加わるようにします。これにはReactの**State**を使います。

### Stateの準備

1.  **入力フォーム用のState:** ユーザーが入力中のテキストを覚えておくためのState。
2.  **ToDoリスト用のState:** アプリ全体のToDoリストを保持するためのState。

`useState` フックを `import` し、`App` 関数の先頭でこれらを定義します。

```typescript
// src/App.tsx
import React, { useState } from 'react'; // useState をインポート
import './App.css';

type Todo = {
  id: number;
  text: string;
  completed: boolean;
};

function App() {
  // 1. ToDoリスト全体を管理するState (初期値は空配列にする)
  const [todos, setTodos] = useState<Todo[]>([]);

  // 2. 入力フォームのテキストを管理するState (初期値は空文字列)
  const [inputText, setInputText] = useState<string>('');

  // --- ここから下はまだ変更しない ---
  // const initialTodos = [...]; // これはもう使わないのでコメントアウトか削除
  // const todoListItems = initialTodos.map(...); // これも後で変える

  // ... return の中身 ...
  return (
    // ...
  );
}
export default App;
```

### フォーム入力とStateの連携 (制御コンポーネント)

入力フォームの `<input>` 要素が変更されるたびに、`inputText` Stateを更新するようにします。

1.  inputの値が変わったときに呼ばれる関数 `handleInputChange` を定義します。
2.  `<input>` 要素に `value` 属性と `onChange` 属性を追加します。

```typescript
// src/App.tsx
import React, { useState } from 'react';
import './App.css';
type Todo = { id: number; text: string; completed: boolean; };

function App() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [inputText, setInputText] = useState<string>('');

  // inputの値が変わったらinputText Stateを更新する関数
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setInputText(event.target.value); // 入力された値でStateを更新
  };

  // ToDoリストの表示（後で修正）
  const todoListItems = todos.map(todo => ( // initialTodos を todos に変更
      <li key={todo.id}>
          {todo.text}
      </li>
  ));

  return (
    <div className="App">
      <h1>ToDoアプリ</h1>

      {/* ToDo入力フォーム */}
      <form> {/* onSubmitは後で追加 */}
        <input
          type="text"
          placeholder="新しいToDoを入力"
          value={inputText}       // inputの値をStateと紐付け
          onChange={handleInputChange} // 値が変わったら関数を呼ぶ
        />
        <button type="submit">追加</button>
      </form>

      {/* ToDoリスト表示エリア */}
      <h2>ToDoリスト</h2>
      <ul>
        {todoListItems} {/* Stateからリストを表示 */}
      </ul>
    </div>
  );
}
export default App;
```

これで、入力フォームに文字を打つと `inputText` Stateがリアルタイムに更新されるようになりました（見た目上の変化はまだありません）。これを**制御コンポーネント**と言います。

### ToDoリストへの追加処理

次に、フォームが送信（「追加」ボタンがクリック）されたときの処理を作ります。

1.  フォーム送信時に呼ばれる関数 `handleAddTodo` を定義します。
2.  `<form>` 要素に `onSubmit` 属性を追加します。

```typescript
// src/App.tsx
import React, { useState } from 'react';
import './App.css';
type Todo = { id: number; text: string; completed: boolean; };

function App() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [inputText, setInputText] = useState<string>('');

  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setInputText(event.target.value);
  };

  // フォームが送信されたときの処理
  const handleAddTodo = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault(); // フォーム送信時のページの再読み込みを防ぐおまじない
    if (inputText.trim() === '') return; // 入力が空なら何もしない

    // 新しいToDoオブジェクトを作成
    const newTodo: Todo = {
      id: Date.now(), // ユニークIDとして現在時刻を使う（簡易的）
      text: inputText,
      completed: false, // 最初は未完了
    };

    // ★ Stateを更新: 既存のtodos配列の末尾に新しいToDoを追加した「新しい配列」を作る
    setTodos([...todos, newTodo]);
    // `...todos` は配列を展開するJavaScriptの書き方

    setInputText(''); // 追加後、入力フォームを空にする
  };

  // ToDoリストの表示
  const todoListItems = todos.map(todo => (
    <li key={todo.id}>
      {todo.text}
    </li>
  ));

  return (
    <div className="App">
      <h1>ToDoアプリ</h1>

      {/* ToDo入力フォーム */}
      <form onSubmit={handleAddTodo}> {/* 送信時に関数を呼ぶ */}
        <input
          type="text"
          placeholder="新しいToDoを入力"
          value={inputText}
          onChange={handleInputChange}
        />
        <button type="submit">追加</button>
      </form>

      {/* ToDoリスト表示エリア */}
      <h2>ToDoリスト</h2>
      <ul>
        {todoListItems}
      </ul>
    </div>
  );
}
export default App;
```

**重要なポイント:**

* `event.preventDefault()`: formのデフォルトの送信動作（ページリロード）をキャンセルします。ReactのようなSPA(Single Page Application)では必須です。
* `setTodos([...todos, newTodo])`: **Stateを直接変更してはいけません！** (`todos.push(newTodo)` はNG)。必ず `setTodos` のような更新関数に、**新しい配列**を渡す必要があります。`[...todos, newTodo]` は、元の `todos` 配列の全要素を展開し、末尾に `newTodo` を追加した**新しい**配列を作るためのJavaScriptの便利な書き方（スプレッド構文）です。

これで、フォームにToDoを入力して「追加」ボタンを押すと、リストに項目が増えるはずです！試してみてください。

## ステップ5: ToDoの完了/未完了を切り替える機能

次に追加したToDoをクリックしたら、打ち消し線が付く（完了状態になる）ようにします。もう一度クリックしたら元に戻る（未完了状態）ようにします。

1.  ToDoアイテムをクリックしたときに呼ばれる関数 `handleToggleComplete` を定義します。どのToDoがクリックされたか区別するために `id` を引数で受け取ります。
2.  リスト項目 `<li>` に `onClick` イベントハンドラを追加します。
3.  `<li>` の見た目を `completed` 状態に応じて変えます。

```typescript
// src/App.tsx
import React, { useState } from 'react';
import './App.css';
type Todo = { id: number; text: string; completed: boolean; };

function App() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [inputText, setInputText] = useState<string>('');

  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setInputText(event.target.value);
  };
  const handleAddTodo = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    if (inputText.trim() === '') return;
    const newTodo: Todo = { id: Date.now(), text: inputText, completed: false };
    setTodos([...todos, newTodo]);
    setInputText('');
  };

  // ToDoの完了/未完了を切り替える関数
  const handleToggleComplete = (id: number) => {
    setTodos(
      // todos配列をmapで処理して新しい配列を作る
      todos.map(todo =>
        // もし現在のtodoのidが、クリックされたidと同じなら
        todo.id === id
          // completedプロパティを反転させた新しいオブジェクトを返す
          ? { ...todo, completed: !todo.completed }
          // idが違う場合は、元のtodoオブジェクトをそのまま返す
          : todo
      )
    );
  };

  // ToDoリストの表示部分を修正
  const todoListItems = todos.map(todo => (
    <li
      key={todo.id}
      // classNameをcompleted状態に応じて変える
      className={todo.completed ? 'completed' : ''}
      // クリックしたらhandleToggleCompleteを呼ぶ (idを渡す)
      onClick={() => handleToggleComplete(todo.id)}
      style={{ cursor: 'pointer' }} // クリックできることがわかるようにカーソルを変える
    >
      {todo.text}
      {/* 削除ボタンは次のステップで追加 */}
    </li>
  ));

  return (
    <div className="App">
      <h1>ToDoアプリ</h1>
      <form onSubmit={handleAddTodo}>
        <input
          type="text"
          placeholder="新しいToDoを入力"
          value={inputText}
          onChange={handleInputChange}
        />
        <button type="submit">追加</button>
      </form>
      <h2>ToDoリスト</h2>
      <ul>
        {todoListItems}
      </ul>
    </div>
  );
}
export default App;
```

**ポイント:**

* `handleToggleComplete`: ここでも `setTodos` を使って**新しい配列**を作っています。`map` を使って、該当する `id` のToDoだけ `completed` の値を反転 (`!todo.completed`) させた新しいオブジェクト (`{ ...todo, completed: ... }`) に置き換え、それ以外のToDoは元のままにして、新しい配列を生成しています。
* `className={todo.completed ? 'completed' : ''}`: 条件（三項）演算子を使って、`todo.completed` が `true` なら `completed` というCSSクラス名を、`false` なら空文字列を `className` に設定しています。`src/index.css` に `.completed` スタイルを定義したので、これで打ち消し線が付くようになります。
* `onClick={() => handleToggleComplete(todo.id)}`: `onClick` に直接関数を渡すのではなく、**アロー関数 `() => ...`** の中で `handleToggleComplete(todo.id)` を呼び出しています。こうしないと、レンダリング時に `handleToggleComplete` が意図せず実行されてしまうためです。また、これによりクリックされたToDoの `id` を関数に渡すことができます。

リストの項目をクリックして、打ち消し線が付いたり消えたりするか試してみてください。

## ステップ6: ToDoを削除する機能

最後に、不要になったToDoを削除する機能を追加しましょう。

1.  削除ボタンを各ToDoアイテムに追加します。
2.  削除ボタンがクリックされたときに呼ばれる関数 `handleDeleteTodo` を定義します。これも `id` を引数で受け取ります。
3.  `<li>` 内の削除ボタンに `onClick` イベントハンドラを追加します。

```typescript
// src/App.tsx
import React, { useState } from 'react';
import './App.css';
type Todo = { id: number; text: string; completed: boolean; };

function App() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [inputText, setInputText] = useState<string>('');

  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { setInputText(event.target.value); };
  const handleAddTodo = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    if (inputText.trim() === '') return;
    const newTodo: Todo = { id: Date.now(), text: inputText, completed: false };
    setTodos([...todos, newTodo]);
    setInputText('');
   };
  const handleToggleComplete = (id: number) => { setTodos(todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo)); };

  // ToDoを削除する関数
  const handleDeleteTodo = (id: number) => {
    // 確認ダイアログを出す（任意）
    // if (!window.confirm('本当に削除しますか？')) {
    //   return;
    // }
    setTodos(
      // todos配列をfilterで処理して新しい配列を作る
      // クリックされたidと「異なる」idを持つToDoだけを残す
      todos.filter(todo => todo.id !== id)
    );
  };

  // ToDoリストの表示部分を修正
  const todoListItems = todos.map(todo => (
    <li
      key={todo.id}
      className={todo.completed ? 'completed' : ''}
    >
      {/* ToDoテキスト部分をクリックでトグルするように変更 */}
      <span onClick={() => handleToggleComplete(todo.id)} style={{ cursor: 'pointer', flexGrow: 1 }}>
        {todo.text}
      </span>
      {/* 削除ボタンを追加 */}
      <button onClick={() => handleDeleteTodo(todo.id)}>削除</button>
    </li>
  ));

  return (
    <div className="App">
      <h1>ToDoアプリ</h1>
      <form onSubmit={handleAddTodo}>
        <input
          type="text"
          placeholder="新しいToDoを入力"
          value={inputText}
          onChange={handleInputChange}
        />
        <button type="submit">追加</button>
      </form>
      <h2>ToDoリスト</h2>
      <ul>
        {todoListItems}
      </ul>
    </div>
  );
}
export default App;
```

**ポイント:**

* `handleDeleteTodo`: JavaScriptの配列メソッド `filter` を使っています。`filter` は、指定した条件に合う要素だけを集めて**新しい配列**を作ります。ここでは `todo.id !== id` という条件、つまり「クリックされた削除ボタンのToDoのid**ではない**」ToDoだけを残しています。これにより、指定したidのToDoが除外された新しい配列が作られ、`setTodos` でStateが更新されます。
* 削除ボタンの `onClick` でも、トグルと同様にアロー関数を使って `handleDeleteTodo(todo.id)` を呼び出し、`id` を渡しています。
* ToDoテキストを `<span>` で囲み、`onClick` をそちらに移動しました。こうすることで、テキスト部分だけがトグル操作、削除ボタンは削除操作、と役割が明確になります。`flexGrow: 1` を `<span>` に追加すると、テキストが可能な限り幅を取り、ボタンが右端に配置されやすくなります（CSSの調整）。

これでToDoの削除機能も実装できました！

## ステップ7: コンポーネント分割 

現状、すべての機能が `App.tsx` という一つのファイルに書かれています。このくらいの規模なら問題ありませんが、アプリがもっと大きくなると、一つのファイルに何百行もコードがあると、読みにくく、修正も大変になります。

そこで重要になるのが**コンポーネント分割**です。UIの部品ごとにファイルを分け、それぞれを独立したコンポーネントとして作ります。

今回のToDoアプリなら、例えば以下のように分割できます。

* **`App.tsx` (親コンポーネント):** 全体のレイアウト、ToDoリスト(`todos`)と入力テキスト(`inputText`)のState管理、各種イベントハンドラ関数（`handleAddTodo`, `handleToggleComplete`, `handleDeleteTodo`）を持つ。
* **`TodoForm.tsx` (子コンポーネント):** ToDo入力フォームの見た目を担当。親から `inputText`, `handleInputChange`, `handleAddTodo` を**Props**として受け取る。
* **`TodoList.tsx` (子コンポーネント):** ToDoリスト全体の表示エリア (`<ul>`) を担当。親から `todos` 配列、`handleToggleComplete`, `handleDeleteTodo` を**Props**として受け取る。
* **`TodoItem.tsx` (孫コンポーネント):** 個々のToDoアイテム (`<li>`) の表示と操作を担当。`TodoList` から個々の `todo` オブジェクト、`handleToggleComplete`, `handleDeleteTodo` を**Props**として受け取る。

**なぜ分割するの？**

* **可読性向上:** 各ファイルが特定の役割に集中するので、コードが読みやすくなります。
* **再利用性向上:** 作成したコンポーネント（例: `TodoForm`）は、別の場所でも再利用できる可能性があります。
* **保守性向上:** 特定の機能を修正したい場合、関連するコンポーネントファイルだけを見れば良くなるため、影響範囲がわかりやすく、修正が容易になります。

コンポーネント分割は、React開発において非常に重要なスキルです。最初は難しく感じるかもしれませんが、「この部分は部品として切り出せそうだな」と考えながら開発を進める癖をつけると良いでしょう。（具体的な分割コードは長くなるためここでは割愛しますが、ぜひ挑戦してみてください！）

## まとめ

入力、表示、更新、削除というCRUD (Create, Read, Update, Delete) と呼ばれる基本的なデータ操作を、ReactのStateとイベント処理を使って実装する流れを体験できたかと思います。


Reactには、今回学んだこと以外にも `useEffect` フックによる副作用の管理、`Context API` を使った効率的なデータ共有、**React Router** を使った複数ページ間の画面遷移など、さらに多くの機能や概念があります。


**参考サイト:**

* **React 公式サイト:** [https://react.dev/](https://react.dev/)
* **React Router (ルーティング):** [https://reactrouter.com/](https://reactrouter.com/)

！