Next.js13でTodoリストを作ってみた!コードを詳しく解説

サムネイル
               

Next.is13を使ったTodoリストをYouTubeを見ながら作ってみました。が、理解が追い付かなかったので自分なりにまとめなおすことにしました。

 

この記事では、「Next.js13」「TypeScript」「React」を学ぶことができます。

 

 

Next.js13でTodoリストを作ってみた!コードを詳しく解説

todoアプリのファイル構造

  1. フォルダ構造:
    • Todoアプリは、pages、components、api、typesの4つの主要なフォルダで構成されています。
    • pages: ページコンポーネントが格納されます。
    • components: 再利用可能なUIコンポーネントが格納されます。
    • api: データ取得用のAPI関数が格納されます。
    • types: アプリで使用される型定義が格納されます。
  2. ページコンポーネント:
    • pagesフォルダ内にpage.tsxがあり、これがTodoアプリのメインページとなります。
    • page.tsxでは、Todoリストの表示とタスクの追加が行われます。
  3. コンポーネント:
    • componentsフォルダ内には、アプリで使用される再利用可能なコンポーネントが格納されます。
    • 例えば、Todoの追加フォーム(AddTask.tsx)、Todoリスト(TodoList.tsx)などがあります。
  4. API関数:
    • apiフォルダ内には、外部APIからデータを取得するための関数が格納されます。
    • 例えば、Todoリストを取得するgetAllTodos関数がここに含まれます。
  5. 型定義:
    • typesフォルダ内には、アプリケーション全体で使用される型定義が格納されます。
    • 例えば、Todoアイテムの型定義(Task)などが含まれます。

 

├── pages

│ └── page.tsx (Todoアプリのメインページ)

├── components

│ ├── AddTask.tsx (Todoの追加フォーム)

│ └── TodoList.tsx (Todoリスト)

├── api

│ └── api.ts (API関数のエクスポート)

└── types

    └── types.ts (型定義)

これらの要素が相互に連携し、Todoアプリが動作します。`page.tsx`でAPI関数を呼び出してデータを取得し、取得したデータを`TodoList.tsx`で表示するようにコンポーネントが設計されています。`AddTask.tsx`では新しいタスクを追加するためのフォームが提供されます。

 

 

各コンポーネント間でのデータの受け渡し

 

+-------------------+
| page.tsx |
| (Main Page) |
+-------------------+
|
| (todos)
|
+-------------------+
| TodoList.tsx |
| (Todo List) |
+-------------------+
|
| (todos)
|
+-------------------+
| TodoItem.tsx |
| (Todo Item) |
+-------------------+
|
| (newTask)
|
+-------------------+
| AddTask.tsx |
| (Add Task Form) |
+-------------------+
  1. page.tsx (Main Page):
    • Todoリスト全体の表示と、新しいタスクの追加フォームを提供します。
    • TodoList.tsx コンポーネントに todos データを渡します。
  2. TodoList.tsx (Todo List):
    • TodoItem.tsx コンポーネントに各Todoアイテムのデータを渡します。
    • AddTask.tsx コンポーネントに新しいタスクを追加するためのフォームを提供します。
  3. TodoItem.tsx (Todo Item):
    • Todoリスト内の個々のTodoアイテムを表示します。
  4. AddTask.tsx (Add Task Form):
    • 新しいタスクを追加するためのフォームを提供します。
    • 追加されたタスクは page.tsx に戻され、Todoリストに反映されます。

このようにして、各コンポーネントがデータを受け取り、それを適切に処理して表示することで、Todoアプリが機能します。

 

ブラウザで表示とコンポーネントの関係

---------------------------
| Nextjs 13 Todo App | <- タイトル (Home)
| ---------------------- |
| | AddTask Component | | <- タスク追加フォーム (AddTask)
| ---------------------- |
| ---------------------- |
| | TodoList Component | | <- タスクリスト (TodoList)
| | ------------------ | |
| | | Todo Component | | | <- 各タスク (Todo)
| | ------------------ | |
| | ------------------ | |
| | | Todo Component | | |
| | ------------------ | |
| | ------------------ | |
| | | Todo Component | | |
| | ------------------ | |
| ---------------------- |
---------------------------

 

 

コード解説

page.tsx

Todoリスト全体の表示と、新しいタスクの追加フォームを提供します。TodoList.tsx コンポーネントに todos データを渡します。

 

src\app\page.tsx

import Image from "next/image";
import AddTask from "./components/AddTask";
import TodoList from "./components/TodoList";
import { getAllTodos } from "./api";


export default async function Home() {
  const todos = await getAllTodos();
  return (
    <main className="flex flex-col items-center min-h-screen py-2 bg-gray-200 justify-center">
      <h1 className="text-4xl font-mono text-gray-700 -mt-32">Nextjs 13 Todo App</h1>
      <div className="w-full max-w-xl mt-5">
        <div className="w-full px-8 py-6 bg-white shadow-md rounded-lg">
          <AddTask />
          <TodoList todos={todos} />
        </div>
      </div>
    </main>
  );
}
import Image from "next/image";
import AddTask from "./components/AddTask";
import TodoList from "./components/TodoList";
import { getAllTodos } from "./api";
  • Image は Next.js の組み込みコンポーネントで、最適化された画像を提供します。この例では画像を使用していませんが、プロジェクトで画像を扱う際に役立ちます。
  • AddTask はタスク追加用のコンポーネントです。
  • TodoList はタスクリストを表示するためのコンポーネントです。
  • getAllTodos は全てのTodoアイテムを取得するための関数です。これはAPIからデータを取得する役割を持っています。
export default async function Home() {
  const todos = await getAllTodos();
  return (
    <main className="flex flex-col items-center min-h-screen py-2 bg-gray-200 justify-center">
      <h1 className="text-4xl font-mono text-gray-700 -mt-32">Nextjs 13 Todo App</h1>
      <div className="w-full max-w-xl mt-5">
        <div className="w-full px-8 py-6 bg-white shadow-md rounded-lg">
          <AddTask />
          <TodoList todos={todos} />
        </div>
      </div>
    </main>
  );
}
  • Home 関数は非同期関数として定義されています。これは getAllTodos からデータを取得するためです。
  • getAllTodos 関数を呼び出し、取得したTodoリストを todos 変数に格納しています。
  • <main> タグの中にアプリケーションの主要なコンテンツが含まれています。
  • className プロパティを使って、Tailwind CSS クラスを適用し、スタイルを整えていま
  • <AddTask /> コンポーネントは新しいタスクを追加するためのフォームやボタンを表示します。
  • <TodoList todos={todos} /> コンポーネントは、取得した todos をリスト表示します。
Home 関数は非同期関数として定義され、getAllTodos 関数はPromiseを返す非同期関数です。getAllTodos 関数が非同期でデータを取得するため、Home コンポーネントが最初にレンダリングされたときにその結果を待つことになります。ここで重要なのは、非同期処理が完了するまで Home コンポーネントのレンダリングが一時停止することです。

 

 

moridai
moridai
非同期処理とレンダリングの関係について説明するね!

 

 

AddTask.tsx

ユーザーが新しいタスクを入力し、「AddTask」ボタンをクリックしてタスクを追加するインターフェース

src\app\components\AddTask.tsx

import React from "react";


const AddTask = () => {
  return (
    <form className="mb-4 space-y-3">
      <input type="text" className="w-full border px-4 py-2 rounded-lg focus:outline-none focus:border-blue-400" />
      <button className="w-full px-4 py-2 text-white bg-blue-500 rounded transform hover:bg-blue-400 hover:scale-95 duration-200">AddTask</button>
    </form>
  );
};


export default AddTask;
import React from "react";
const AddTask = () => {
//~~~~~~~~~~~~~~~~~~~~~~~~~~
省略
//~~~~~~~~~~~~~~~~~~~~~~~~~~
};
export default AddTask;
  • Reactをインポートしています。このファイルがReactコンポーネントを定義するためのものであることを示しています。
  • AddTask という関数コンポーネントを定義しています。
  • return 文の中で、このコンポーネントが返すJSXを定義しています。
  • AddTask コンポーネントをデフォルトエクスポートしています。これにより、他のファイルでこのコンポーネントをインポートして使用できます。

 

 

 

Todo.tsx

個々のTodoアイテムをリストアイテムとして表示します。アイテムのテキスト、編集ボタン、および削除ボタンを含んでいます。TodoList.tsxの子コンポーネント

src\app\components\Todo.tsx

import React from "react";
import { Task } from "../types";


interface TodoProps {
  todo: Task;
}


const Todo = ({ todo }: TodoProps) => {
  return (
    <li key={todo.id} className="flex justify-between p-4 bg-white border-l-4 border-blue-500 rounded shadow">
      <span>{todo.text}</span>
      <div>
        <button className="text-green-500 mr-4">edit</button>
        <button className="text-red-300">delete</button>
      </div>
    </li>
  );
};


export default Todo;
import React from "react";
import { Task } from "../types";
  • React をインポートしています。これにより、Reactの機能を使用してコンポーネントを定義できます。
  • Task 型をインポートしています。これは ../types ファイルで定義されています。

 

interface TodoProps {
todo: Task;
}
  • TodoProps というインターフェースを定義しています。これは Todo コンポーネントが受け取るプロパティの型を指定しています。
  • todo プロパティは Task 型であることを示しています。

 

const Todo = ({ todo }: TodoProps) => {
  return (
    <li key={todo.id} className="flex justify-between p-4 bg-white border-l-4 border-blue-500 rounded shadow">
      <span>{todo.text}</span>
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
省略
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    </li>
  );
};
  • Todo という関数コンポーネントを定義しています。
  • このコンポーネントは todo というプロパティを受け取り、それを TodoProps インターフェースに基づいて型定義しています。
  • key={todo.id} は、Reactがこのアイテムを一意に識別するためのキーです(ただし、ここでは必要ありません。map メソッドを使用する親コンポーネントがキーを渡すため)。
  • <span> 要素は、Todoアイテムのテキストを表示します。

 

export default Todo;
  • Todo コンポーネントをデフォルトエクスポートしています。これにより、他のファイルでこのコンポーネントをインポートして使用できます。

 

 

TodoList.tsx

TodoList コンポーネントは、todos 配列を受け取り、それぞれのTodoアイテムを Todo コンポーネントとしてレンダリングします。これにより、TodoList コンポーネントは、タスクリスト全体の表示を担当し、個々のタスクの詳細表示は Todo コンポーネントに委ねています。

src\app\components\TodoList.tsx

 

import React, { useEffect } from "react";
import { Task } from "../types";
import Todo from "./Todo";


interface TodoListProps {
  todos: Task[];
}


const TodoList = ({ todos }: TodoListProps) => {
  return (
    <ul className="space-y-3">
      {todos.map((todo) => (
        <Todo key={todo.id} todo={todo}/>
      ))}
    </ul>
  );
};


export default TodoList;

 

import React from "react";
import { Task } from "../types";
import Todo from "./Todo";
  • React をインポートしています。このファイルがReactコンポーネントを定義するためのものであることを示しています。
  • Task 型をインポートしています。これは ../types ファイルで定義されています。
  • Todo コンポーネントをインポートしています。これは個々のTodoアイテムを表示するためのコンポーネントです。

 

interface TodoListProps {
todos: Task[];
}
  • TodoListProps というインターフェースを定義しています。これは TodoList コンポーネントが受け取るプロパティの型を指定しています。
  • todos プロパティは Task 型の配列であることを示しています。

 

const TodoList = ({ todos }: TodoListProps) => {
  return (
    <ul className="space-y-3">
      {todos.map((todo) => (
        <Todo key={todo.id} todo={todo}/>
      ))}
    </ul>
  );
};
  • TodoList という関数コンポーネントを定義しています。
  • このコンポーネントは todos というプロパティを受け取り、それを TodoListProps インターフェースに基づいて型定義しています。
  • todos 配列の各要素に対して map メソッドを使用して、各Todoアイテムを Todo コンポーネントとしてレンダリングします。
  • Todo コンポーネントには key プロパティと todo プロパティを渡します。
    • key={todo.id} は、Reactが各アイテムを一意に識別するためのキーです。
    • todo={todo} は、Todo コンポーネントにそのTodoアイテムのデータを渡します。

 

export default TodoList;
  • TodoList コンポーネントをデフォルトエクスポートしています。これにより、他のファイルでこのコンポーネントをインポートして使用できます。

 

 

api.ts(getAllTodos、addTodo)

api.ts ファイルは、サーバーからタスクデータを取得するための非同期関数を定義しています。このファイルは、getAllTodos 関数をエクスポートし、アプリケーションの他の部分で使用できるようにしています。

src\app\api.ts

import { Task } from "./types";


export const getAllTodos = async (): Promise<Task[]> => {
const res = await fetch('http://localhost:3001/tasks',{cache: "no-store",//SSR
});
const todos = res.json();


return todos;
};


export const addTodo = async (todo: Task): Promise<Task> => {
  const res = await fetch('http://localhost:3001/tasks', {
    method: "POST",
    headers: {
      "Content-Type":"application/json",
    },
    body: JSON.stringify(todo),
  });
  const newTodo = res.json();


  return newTodo;
};

 

getAllTodos関数

import { Task } from "./types";


export const getAllTodos = async (): Promise<Task[]> => {
const res = await fetch('http://localhost:3001/tasks',{cache: "no-store",//SSR
});
const todos = res.json();


return todos;
};
  • fetch 関数: 指定されたURLからデータを取得します。
  • cache: "no-store": SSR(サーバーサイドレンダリング)の場合、常に最新のデータを取得するためにキャッシュを無効にします。
  • await res.json(): レスポンスをJSON形式に解析し、JavaScriptオブジェクトに変換します。
  • Promise<Task[]>: タスクの配列を返すPromiseを返します。

 

addTodo関数

export const addTodo = async (todo: Task): Promise<Task> => {
  const res = await fetch('http://localhost:3001/tasks', {
    method: "POST",
    headers: {
      "Content-Type":"application/json",
    },
    body: JSON.stringify(todo),
  });
  const newTodo = res.json();


  return newTodo;
};

 

fetch 関数: 指定されたURLに対してPOSTリクエストを送信します。

  • method: "POST": HTTPメソッドとしてPOSTを指定します。
  • headers: リクエストヘッダにコンテンツタイプとしてJSONを指定します。
  • body: JSON.stringify(todo): タスクオブジェクトをJSON文字列に変換してリクエストボディに設定します。
  • await res.json(): レスポンスをJSON形式に解析し、新しく追加されたタスクをJavaScriptオブジェクトに変換します。
  • Promise<Task>: 新しく追加されたタスクを返すPromiseを返します。

 

AddTask.tsx (use client)

新しいタスクを追加するフォームを提供しています。ここでは、ユーザーがテキストを入力し、フォームを送信するとタスクが追加される流れになっています。

1. ユーザーが入力フィールドにタスクタイトルを入力
|
v
2. 入力フィールドの値が変わると setTaskTitle によって taskTitle ステートが更新される
|
v
3. ユーザーがフォームを送信
|
v
4. handleSubmit 関数が呼び出される
|
v
5. addTodo 関数を使って新しいタスクが追加される
|
v
6. サーバーにタスクデータが送信され、サーバーがレスポンスを返す
|
v
7. タスクが追加された後、入力フィールドがクリアされる

 

+--------------------------------------------------+
| ユーザーが入力フィールドにタスクタイトルを入力 |
| +--------------------------------------------+ |
| | <input value={taskTitle} onChange={...} /> | |
| +--------------------------------------------+ |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| 入力フィールドの値が変わると |
| setTaskTitle によって taskTitle ステートが更新 |
| +--------------------------------------------+ |
| | const [taskTitle, setTaskTitle] = | |
| | useState(""); | |
| +--------------------------------------------+ |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| ユーザーがフォームを送信 |
| +--------------------------------------------+ |
| | <form onSubmit={handleSubmit}> | |
| +--------------------------------------------+ |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| handleSubmit 関数が呼び出される |
| +--------------------------------------------+ |
| | const handleSubmit = async (e: FormEvent) =>| |
| | { e.preventDefault(); await addTodo(...); | |
| | setTaskTitle(""); } | |
| +--------------------------------------------+ |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| addTodo 関数を使って新しいタスクが追加される |
| +--------------------------------------------+ |
| | await addTodo({ id: uuidv4(), text: ... }) | |
| +--------------------------------------------+ |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| サーバーにタスクデータが送信され、サーバーがレスポンスを返す |
| +--------------------------------------------+ |
| | const res = await fetch('http://localhost:3001/tasks', { ... }) |
| | const newTodo = await res.json(); | |
| +--------------------------------------------+ |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| タスクが追加された後、入力フィールドがクリアされる |
| +--------------------------------------------+ |
| | setTaskTitle(""); | |
| +--------------------------------------------+ |
+--------------------------------------------------+

 

"use client";


import React, { ChangeEvent, FormEvent, useState } from "react";
import { addTodo } from "../api";
import { v4 as uuidv4 } from "uuid";


const AddTask = () => {
  const [taskTitle, setTaskTitle] = useState("");


  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();


    await addTodo({ id: uuidv4(), text: taskTitle });


    setTaskTitle("");
  };


  return (
    <form className="mb-4 space-y-3" onSubmit={handleSubmit}>
      <input type="text" className="w-full border px-4 py-2 rounded-lg focus:outline-none focus:border-blue-400" onChange={(e: ChangeEvent<HTMLInputElement>) => setTaskTitle(e.target.value)}
      value={taskTitle}
      />
      <button className="w-full px-4 py-2 text-white bg-blue-500 rounded transform hover:bg-blue-400 hover:scale-95 duration-200">AddTask</button>
    </form>
  );
};


export default AddTask;

 

"use client";


importReact, { ChangeEvent, FormEvent, useState } from"react";
import { addTodo } from"../api";
import { v4asuuidv4 } from"uuid";
  • "use client";: これはNext.jsの構文で、ファイルがクライアント側でレンダリングされることを示しています。
  • React とそのフックをインポートしています (ChangeEvent, FormEvent, useState)。
  • addTodo 関数を ../api からインポートしています。
  • uuid パッケージから uuidv4 をインポートし、ユニークなIDを生成するために使用しています。

 

const [taskTitle, setTaskTitle] = useState("");
  • useState フック: フォーム入力の状態を管理します。taskTitle は現在のタスクタイトルを保持し、setTaskTitle はその値を更新する関数です。

 

 const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();


    await addTodo({ id: uuidv4(), text: taskTitle });


    setTaskTitle("");
  };
  • handleSubmit 関数: フォームが送信されたときに呼び出されます。
    • e.preventDefault() は、フォームのデフォルトの送信動作を防ぎます。
    • addTodo 関数を呼び出し、新しいタスクを追加します。ここで uuidv4 によって生成されたユニークなIDと taskTitle を含むタスクオブジェクトを渡します。
    • タスクが追加された後、taskTitle を空文字列にリセットして、入力フィールドをクリアします。

 

return (
    <form className="mb-4 space-y-3" onSubmit={handleSubmit}>
      <input type="text" className="w-full border px-4 py-2 rounded-lg focus:outline-none focus:border-blue-400" onChange={(e: ChangeEvent<HTMLInputElement>) => setTaskTitle(e.target.value)}
      value={taskTitle}
      />
      <button className="w-full px-4 py-2 text-white bg-blue-500 rounded transform hover:bg-blue-400 hover:scale-95 duration-200">AddTask</button>
    </form>
  );
  • フォーム (<form>): onSubmit プロパティに handleSubmit 関数を設定しています。
  • 入力フィールド (<input>):
    • type="text": テキスト入力フィールドです。
    • className: Tailwind CSS クラスを使用してスタイルを適用しています。
    • onChange: 入力フィールドの値が変わったときに呼び出される関数。入力された値を taskTitle に設定します。
    • value: 現在の taskTitle の値を表示します。
  • ボタン (<button>):
    • className: Tailwind CSS クラスを使用してスタイルを適用しています。
    • ボタンをクリックするとフォームが送信されます。

 

 

 

api.ts(editTodo)

 

import { text } from "strem/consumers";
import { Task } from "./types";
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//省略
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

export const editTodo = async (id: string, newText: string): Promise<Task> => {
  const res = await fetch(`http://localhost:3001/tasks/$(id)`, {
    method: "PUT",
    headers: {
      "Content-Type":"application/json",
    },
    body: JSON.stringify({ text: newText}),
  });
  const updateTodo = res.json();


  return updateTodo;
};
  • 目的: 既存のタスクを編集する。
  • HTTPメソッド: PUT
  • リクエストURL: タスクのIDをURLの一部として渡す(http://localhost:3001/tasks/${id})。
  • リクエストヘッダー: Content-Type: "application/json" を設定。
  • リクエストボディ: 新しいテキストを含むオブジェクトを JSON.stringify でJSON文字列に変換して送信。
  • レスポンス処理: 更新されたタスクを await res.json() でパースし、updatedTodo 変数に格納して返します。

 

 

Todo.tsx(編集機能の追加)

ユーザーが特定のタスク(ToDoアイテム)を編集できるようにするためのものです。

"use client";


import React, { useEffect, useRef, useState } from "react";
import { Task } from "../types";
import { editTodo } from "../api";


interface TodoProps {
  todo: Task;
}


const Todo = ({ todo }: TodoProps) => {
  const ref = useRef<HTMLInputElement>(null);


  const [isEditing, setIsEditing] = useState(false);
  const [editedTaskTitle, setEditedTaskTitle] = useState(todo.text);


  useEffect(() => {
    if (isEditing) {
      ref.current?.focus();
    }
  }, [isEditing]);


  const handleEdit = async () => {
    setIsEditing(true);
  };


  const handleSave = async () => {
    await editTodo(todo.id, editedTaskTitle);
    setIsEditing(false);
  };


  return (
    <li
      key={todo.id}
      className="flex justify-between p-4 bg-white border-l-4 border-blue-500 rounded shadow"
    >
      {isEditing ?(
        <input
          ref={ref}
          type="text"
          className="mr-2 px-2 rounded border-gray-400 border"
          value={editedTaskTitle}
          onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
            setEditedTaskTitle(e.target.value)
            }
        />
      ) : (
        <span>{todo.text}</span>
      )}
      <div>
        {isEditing ? (
          <button className="text-blue-500 mr-4" onClick={handleSave}>
            save
          </button>
        ) : (
          <button className="text-green-500 mr-4" onClick={handleEdit}>
            edit
          </button>
        )}


        <button className="text-red-300">delete</button>
      </div>
    </li>
  );
};


export default Todo;
"use client";

import React, { useEffect, useRef, useState } from "react";
import { Task } from "../types";
import { editTodo } from "../api";
  • use client: これはNext.js 13のディレクティブで、このコンポーネントがクライアント側でレンダリングされることを示します。
  • React, { useEffect, useRef, useState }: Reactのフックをインポートしています。これらはコンポーネントの状態管理や副作用処理、DOM要素へのアクセスに使用されます。
  • editTodo: タスクを編集するためのAPI関数をインポートしています。
const Todo = ({ todo }: TodoProps) => {
const ref = useRef<HTMLInputElement>(null);
  • Todo: Todoコンポーネントはtodoプロパティを受け取り、タスクを表示および編集します。
  • ref = useRef<HTMLInputElement>(null): 編集モードの時に入力フィールドにフォーカスを設定するための参照を作成します。
  const [isEditing, setIsEditing] = useState(false);
  const [editedTaskTitle, setEditedTaskTitle] = useState(todo.text);


  useEffect(() => {
    if (isEditing) {
      ref.current?.focus();
    }
  }, [isEditing]);
  • isEditing: 編集モードかどうかを管理する状態フックです。
  • editedTaskTitle: 編集中のタスクのタイトルを保持する状態フックです。初期値はtodo.textです。
  • useEffect: isEditingtrueになったときに入力フィールドにフォーカスを設定します。
  const handleEdit = async () => {
    setIsEditing(true);
  };


  const handleSave = async () => {
    await editTodo(todo.id, editedTaskTitle);
    setIsEditing(false);
  };
  • handleEdit: 編集モードに切り替える関数です。
  • handleSave: 編集したタスクを保存し、編集モードを終了する関数です。editTodo関数を呼び出してサーバーに更新を送信します。
  return (
    <li
      key={todo.id}
      className="flex justify-between p-4 bg-white border-l-4 border-blue-500 rounded shadow"
    >
      {isEditing ?(
        <input
          ref={ref}
          type="text"
          className="mr-2 px-2 rounded border-gray-400 border"
          value={editedTaskTitle}
          onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
            setEditedTaskTitle(e.target.value)
            }
        />
      ) : (
        <span>{todo.text}</span>
      )}
      <div>
        {isEditing ? (
          <button className="text-blue-500 mr-4" onClick={handleSave}>
            save
          </button>
        ) : (
          <button className="text-green-500 mr-4" onClick={handleEdit}>
            edit
          </button>
        )}


        <button className="text-red-300">delete</button>
      </div>
    </li>
  );
};
  • <li>: タスクを表すリストアイテムです。スタイルが設定されています。
  • 編集モードの場合:
    • <input>: タスクのタイトルを編集するための入力フィールドです。refを設定してフォーカスを管理し、valueeditedTaskTitleonChangeイベントでeditedTaskTitleを更新します。
    • <button>: 保存ボタンです。onClickイベントでhandleSaveを呼び出します。
  • 非編集モードの場合:
    • <span>: タスクのタイトルを表示します。
    • <button>: 編集ボタンです。onClickイベントでhandleEditを呼び出します。
  • <button>: 削除ボタンです(現在は機能が実装されていません)。

 

 

Api.ts(削除ボタンを追加)

指定されたIDのタスクを削除するための非同期関数 deleteTodo を定義しています。この関数はAPIリクエストを使用して、サーバー上のタスクを削除します。

import { text } from "stream/consumers";
import { Task } from "./types";

//~~~~~~~~~~~~~~~~~~~~~~~~~~
//省略
//~~~~~~~~~~~~~~~~~~~~~~~~~~


export const deleteTodo = async (id: string): Promise<Task> => {
  const res = await fetch(`http://localhost:3001/tasks/${id}`, {
    method: "DELETE",
    headers: {
      "Content-Type":"application/json",
    },
  });
  const deleteTodo = res.json();


  return deleteTodo;
};
export const deleteTodo = async (id: string): Promise<Task> => {
  • deleteTodo: この関数はタスクの削除を行います。
  • async: 関数が非同期であり、Promiseを返すことを示します。
  • id: string: 関数が受け取る引数で、削除するタスクのIDです。
  • Promise<Task>: 関数の戻り値の型で、削除されたタスクの情報を含むPromiseです。

 

  const res = await fetch(`http://localhost:3001/tasks/${id}`, {
    method: "DELETE",
    headers: {
      "Content-Type":"application/json",
    },
  });
  • fetch: fetch関数を使用してHTTPリクエストを送信します。
  • `http://localhost:3001/tasks/${id}`: テンプレートリテラルを使用して、削除対象のタスクのIDをURLに組み込みます。これにより、特定のタスクに対してリクエストが送信されます。
  • method: "DELETE": HTTPメソッドとしてDELETEを指定し、サーバーに削除リクエストを送信します。
  • headers: リクエストヘッダーを設定し、Content-Typeapplication/jsonに指定します。これにより、リクエストの内容がJSON形式であることを示します。

 

const deleteTodo = await res.json();

res.json()メソッドを呼び出すと、レスポンスボディがJSON形式であると仮定して、その内容をJavaScriptのオブジェクトとして解析(パース)します。このメソッドは非同期で実行されるため、awaitを使って完了を待ちます。

 

 

todo.tsx(削除ボタンを追加)

タスク(Todo)を表示し、編集および削除する機能を持つコンポーネントを定義しています。削除機能に焦点を当てた部分の解説します。

"use client";


import React, { useEffect, useRef, useState } from "react";
import { Task } from "../types";
import { deleteTodo, editTodo } from "../api";

//~~~~~~~~~~~~~~~~~~~~~~~~
//省略
//~~~~~~~~~~~~~~~~~~~~~~~~

  const handleDelete = async () => {
    await deleteTodo(todo.id);
  };


  return (
//~~~~~~~~~~~~~~~~~~~~~~~~
//省略
//~~~~~~~~~~~~~~~~~~~~~~~~
        <button className="text-red-300" onClick={handleDelete}>delete</button>
      </div>
    </li>
  );
};


export default Todo;
import { deleteTodo, editTodo } from "../api";
タスクを削除する関数 deleteTodo と編集する関数 editTodo をインポートしています。これらの関数は別のファイル (../api) で定義されています。
  const handleDelete = async () => {
    await deleteTodo(todo.id);
  };
  • handleDelete: タスクを削除するための関数です。
  • await deleteTodo(todo.id): deleteTodo 関数を呼び出し、指定された todo.id に基づいてタスクを削除します。この関数は非同期関数であるため、await を使用して処理が完了するまで待ちます。
<button className="text-red-300" onClick={handleDelete}>delete</button>
  • onClick={handleDelete}: ボタンがクリックされたときに handleDelete 関数を呼び出します。これにより、タスクが削除されます。

 

まとめ

Todoリストって難しいですよね。

一からまとめ直すだけで学びになると思います。個人的にはYouTubeだけよりもまとめることが理解が相当深まりました。

Next.jsはpegeルーティングの理解がなかなか難しい印象です。何度も何度も学び直しました!