Next.jsで学ぶReact講座(完全初心者向け・30日)演習問題・課題 | 第4章:実用的React – ミニアプリ③

React Next.js
スポンサーリンク

この課題のねらい

ここはいよいよ「実用的 React」の集大成です。
テーマはミニ TODO アプリ──追加・削除・完了状態管理までを、自分の手で通しで作ること。

state 設計
親子コンポーネントと props
イベント処理(追加・削除・トグル)

これらが一気につながるので、「React でアプリを作るってこういうことか」がかなりクリアになります。


TODOアプリの全体像を決める

どんな機能を持たせるか

今回のミニアプリの要件はこうです。

  • TODO をテキストで追加できる
  • TODO を削除できる
  • TODO に「完了フラグ」を持たせて、完了/未完了を切り替えられる

画面イメージとしては、

  • 上に入力フォーム+追加ボタン
  • 下に TODO の一覧
  • 各 TODO に「完了チェックボックス」と「削除ボタン」

くらいをイメージしておくといいです。

状態設計(どこに何を持つか)

状態はこう設計します。

  • todos(TODO の配列)
    • 1件は { id, title, completed } の形
    • 親コンポーネント(TodoApp)が持つ
  • 入力中のテキスト
    • フォームコンポーネント(TodoInput)が持つ

図にするとこんな感じです。

TodoApp
  ├─ state: todos
  ├─ handleAddTodo(title)
  ├─ handleToggleTodo(id)
  ├─ handleDeleteTodo(id)
  ├─ TodoInput(追加用フォーム)
  └─ TodoList(一覧表示)
        └─ TodoItem(1件分)

必須課題① TODOの追加機能

親コンポーネントでtodosを管理する

まずは親コンポーネント TodoApp を作ります。

"use client";

import { useState } from "react";
import { TodoInput } from "./TodoInput";
import { TodoList } from "./TodoList";

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

export default function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([]);

  function handleAddTodo(title: string) {
    const newTodo: Todo = {
      id: Date.now(),
      title,
      completed: false,
    };
    setTodos((prev) => [...prev, newTodo]);
  }

  return (
    <main style={{ padding: "24px", fontFamily: "sans-serif" }}>
      <h1>TODO アプリ</h1>
      <TodoInput onAdd={handleAddTodo} />
      <TodoList todos={todos} />
    </main>
  );
}
TSX

ここでのポイントは、

  • todos はアプリ全体で共有したいので、親の TodoApp が持つ
  • handleAddTodo で新しい TODO を作り、setTodos で配列に追加する
  • 追加処理は TodoInput から呼び出せるように、onAdd として渡す

入力フォームコンポーネントを作る

次に、TodoInput を作ります。

// TodoInput.tsx
"use client";

import { useState } from "react";

type TodoInputProps = {
  onAdd: (title: string) => void;
};

export function TodoInput({ onAdd }: TodoInputProps) {
  const [value, setValue] = useState("");

  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const trimmed = value.trim();
    if (!trimmed) return;
    onAdd(trimmed);
    setValue("");
  }

  return (
    <form onSubmit={handleSubmit} style={{ marginTop: "16px" }}>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="TODO を入力"
        style={{ padding: "4px 8px", width: "220px" }}
      />
      <button
        type="submit"
        style={{
          marginLeft: "8px",
          padding: "4px 12px",
          borderRadius: "9999px",
          border: "none",
          backgroundColor: "#3b82f6",
          color: "white",
          cursor: "pointer",
        }}
      >
        追加
      </button>
    </form>
  );
}
TSX

ここでやっていることは、

  • 入力中のテキストは TodoInput 自身の state(value)で管理
  • 送信時に onAdd(value) を呼んで、親に「このタイトルで TODO 追加して」と依頼
  • 追加後は setValue("") で入力欄をクリア

「入力状態はフォームが持つ」「TODO の配列は親が持つ」という役割分担が大事です。


必須課題② TODOの削除機能

削除用のハンドラを親に用意する

TodoApp に削除用の関数を追加します。

function handleDeleteTodo(id: number) {
  setTodos((prev) => prev.filter((todo) => todo.id !== id));
}
TSX

そして、TodoListonDelete として渡します。

<TodoList todos={todos} onDelete={handleDeleteTodo} />
TSX

一覧表示コンポーネントで削除ボタンを作る

TodoListTodoItem を作ります。

// TodoList.tsx
type Todo = {
  id: number;
  title: string;
  completed: boolean;
};

type TodoListProps = {
  todos: Todo[];
  onDelete: (id: number) => void;
  onToggle: (id: number) => void;
};

export function TodoList({ todos, onDelete, onToggle }: TodoListProps) {
  if (todos.length === 0) {
    return <p style={{ marginTop: "16px" }}>TODO はまだありません。</p>;
  }

  return (
    <div style={{ marginTop: "16px" }}>
      {todos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onDelete={onDelete}
          onToggle={onToggle}
        />
      ))}
    </div>
  );
}
TSX
// TodoItem.tsx
type Todo = {
  id: number;
  title: string;
  completed: boolean;
};

type TodoItemProps = {
  todo: Todo;
  onDelete: (id: number) => void;
  onToggle: (id: number) => void;
};

export function TodoItem({ todo, onDelete, onToggle }: TodoItemProps) {
  return (
    <div
      style={{
        display: "flex",
        alignItems: "center",
        gap: "8px",
        padding: "4px 0",
      }}
    >
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span
        style={{
          textDecoration: todo.completed ? "line-through" : "none",
          color: todo.completed ? "#9ca3af" : "#111827",
        }}
      >
        {todo.title}
      </span>
      <button
        onClick={() => onDelete(todo.id)}
        style={{
          marginLeft: "auto",
          padding: "2px 8px",
          borderRadius: "9999px",
          border: "none",
          backgroundColor: "#ef4444",
          color: "white",
          cursor: "pointer",
          fontSize: "12px",
        }}
      >
        削除
      </button>
    </div>
  );
}
TSX

削除の流れはこうです。

  • ユーザーが「削除」ボタンを押す
  • TodoItem から onDelete(todo.id) が呼ばれる
  • TodoList 経由で TodoApphandleDeleteTodo が呼ばれる
  • setTodos で該当 ID の TODO を配列から除外

「削除の決定権は親(TodoApp)にある」という構造がポイントです。


挑戦課題 完了状態管理

completedフラグをトグルする

完了状態は、Todo 型にすでに completed: boolean として持たせています。
あとは「チェックボックスを押したら true/false を切り替える」処理を書くだけです。

TodoApp にトグル用の関数を追加します。

function handleToggleTodo(id: number) {
  setTodos((prev) =>
    prev.map((todo) =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    )
  );
}
TSX

そして、TodoListonToggle として渡します。

<TodoList todos={todos} onDelete={handleDeleteTodo} onToggle={handleToggleTodo} />
TSX

TodoItem では、チェックボックスと見た目を completed に応じて変えています。

<input
  type="checkbox"
  checked={todo.completed}
  onChange={() => onToggle(todo.id)}
/>

<span
  style={{
    textDecoration: todo.completed ? "line-through" : "none",
    color: todo.completed ? "#9ca3af" : "#111827",
  }}
>
  {todo.title}
</span>
TSX

これで、

  • チェックを入れると completed: true になり、取り消し線+グレー表示
  • チェックを外すと completed: false に戻り、通常表示

という「完了状態管理」が実現できます。


まとめ:ミニ TODO アプリでつかんでほしいこと

このミニアプリで押さえてほしいのは、次の感覚です。

  • TODO の配列(todos)は親コンポーネントが一元管理する
  • 追加・削除・完了トグルといった「状態を変える処理」は、全部親に置く
  • 子コンポーネントは「入力フォーム」「表示」「ボタン押下のきっかけ」を担当し、実際の状態変更は props 経由で親に任せる
  • 完了状態のようなフラグは、見た目(取り消し線・色)としっかり結びつけてあげると、UX が一気に上がる

ここまで通せていたら、もう「おもちゃのカウンター」ではなく、
立派に「小さなプロダクト」を自分で組み立てられている状態です。

タイトルとURLをコピーしました