1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

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

なぜToDoアプリ?

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

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

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

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

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

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

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)

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

cd my-todo-app
npm install

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

npm run dev

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

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

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

  1. src/App.css: ファイルの中身をすべて削除して空にします。(後で自分でスタイルを書きます)

  2. src/index.css: こちらも一旦中身をすべて削除してOKです。(必要なら後で基本的なスタイルを追加します)

  3. src/App.tsx: このファイルがメインの画面を作る場所になります。中身を以下のようにシンプルに書き換えてください。

    // 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リストを表示するエリアの骨組みを作ります。

// 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です)

/* 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 を持つオブジェクトとします。

// 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 関数の先頭でこれらを定義します。

// 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 属性を追加します。
// 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 属性を追加します。
// 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 状態に応じて変えます。
// 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.completedtrue なら 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 イベントハンドラを追加します。
// 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, handleAddTodoPropsとして受け取る。
  • TodoList.tsx (子コンポーネント): ToDoリスト全体の表示エリア (<ul>) を担当。親から todos 配列、handleToggleComplete, handleDeleteTodoPropsとして受け取る。
  • TodoItem.tsx (孫コンポーネント): 個々のToDoアイテム (<li>) の表示と操作を担当。TodoList から個々の todo オブジェクト、handleToggleComplete, handleDeleteTodoPropsとして受け取る。

なぜ分割するの?

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

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

まとめ

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

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

参考サイト:

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?