【05】Next.js14でTodoアプリ作成(UPDATE編)

# はじめに

Next.jsの知識を深めるために、Todoアプリを作ることにしました。そのメモを残しておきます。今回はデータ更新についてのメモを残します。何か間違っていることなどございましたらコメントいただけますと、とても喜びます。

<details><summary>今までの記事</summary>
環境構築編
https://qiita.com/naoyuki2/items/e974c630c6cbd3c55254

【01】Next.js14でTodoアプリ作成(CREATE編)

https://qiita.com/naoyuki2/items/7474d448de7769905d82

【02】Next.js14でTodoアプリ作成(SELECT編)

https://qiita.com/naoyuki2/items/2d8ad09f767f818e8860

【03】Next.js14でTodoアプリ作成(INSERT編)

https://qiita.com/naoyuki2/items/27d550ab3766a1869aaf

【04】Next.js14でTodoアプリ作成(DELETE編)

https://qiita.com/naoyuki2/items/8fcb8ae5583a1aef8a2d

</details>

# 今回のゴール

データの更新ができるようになる。とはいっても、完了、未完了の値(`isDone`)のみ更新できるようにする。

# １．APIの作成(PATCHメソッド)

### PATCHメソッドの記述

`src/app/api/todo/[id]/route.ts`に`PATCH`メソッドを記述していく。`DELETE`メソッドがすでにあるのでその下に書く。

```tsx:src/app/api/todo/[id]/route.ts
export async function PATCH(
    request: Request,
    { params }: { params: { id: string } },
) {
    // todoテーブルのレコードを更新
    const body = await request.json()
    const id = Number(params.id)
    try {
        const todo: todo = await prisma.todo.update({
            where: {
                id: id,
            },
            data: {
                isDone: body.isDone,
            },
        })
        return NextResponse.json(todo)
    } catch (error) {
        return NextResponse.json(error)
    }
}
```

### コードの解説

[前回の記事](https://qiita.com/naoyuki2/items/8fcb8ae5583a1aef8a2d)で解説したところは省略する。

```tsx:
const todo: todo = await prisma.todo.update({
    where: {
        id: id,
    },
    data: {
        isDone: body.isDone,
    },
})
return NextResponse.json(todo)
``` 

今回は`prisma`の`update`関数を使う。

`where`には前回と同様にリクエストパラメータから取得した`id`を使用する。

今回は`isDone`のみ更新するため`data`に変更するカラム名(`isDone`)と値を記述する。

# ２．フロントエンドの実装

[前回の記事](https://qiita.com/naoyuki2/items/8fcb8ae5583a1aef8a2d)から`patchTodo`関数と`チェックボックス`を追加した。

### コードの全体

```diff_tsx:src/app/page.tsx
'use client'

import { useEffect, useState } from 'react'
import { todo } from '@prisma/client'

export default function Home() {
    const [todos, setTodos] = useState<todo[]>([])

    const [inputVal, setInputVal] = useState<string>('')

    useEffect(() => {
        const getTodo = async () => {
            const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/todo`)
            const data = await res.json()
            setTodos(data)
        }
        getTodo()
    }, [])

    const postTodo = async () => {
        if (!inputVal) return alert('タイトルを入力してください')

        const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/todo`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                title: inputVal,
            }),
        })
        const data = await res.json()
        setTodos([...todos, data])
        setInputVal('')
    }

    const deleteTodo = async (id: number) => {
        const res = await fetch(
            `${process.env.NEXT_PUBLIC_API_URL}/todo/${id}`,
            {
                method: 'DELETE',
            },
        )
        const data = await res.json()
        setTodos(todos.filter((todo) => todo.id !== data.id))
    }

+   const patchTodo = async (todo: todo) => {
+       const res = await fetch(
+           `${process.env.NEXT_PUBLIC_API_URL}/todo/${todo.id}`,
+           {
+               method: 'PATCH',
+               headers: {
+                   'Content-Type': 'application/json',
+               },
+               body: JSON.stringify({
+                   isDone: !todo.isDone,
+               }),
+           },
+       )
+       const data = await res.json()
+       setTodos(
+           todos.map((todo) => {
+               if (todo.id === data.id) {
+                   return data
+               }
+               return todo
+           }),
+       )
+   }

    return (
        <>
            <input
                placeholder="タイトル入力"
                value={inputVal}
                onChange={(e) => setInputVal(e.target.value)}
            />

            <button onClick={() => postTodo()}>追加</button>

            {todos.map((todo) => (
                <div key={todo.id}>
+                   <input
+                       type="checkbox"
+                       checked={todo.isDone}
+                       onChange={() => patchTodo(todo)}
+                   />
                    <p>{todo.id}</p>
                    <p>{todo.title}</p>
                    <p>{todo.content}</p>
                    <p>{todo.isDone}</p>
                    <p>{new Date(todo.createdAt).toLocaleString()}</p>
                    <p>{new Date(todo.updatedAt).toLocaleString()}</p>
                    <button
                        onClick={() => deleteTodo(todo.id)}
                        className="bg-rose-400 px-2 py-1 rounded"
                    >
                        削除
                    </button>
                </div>
            ))}
        </>
    )
}

```

### コードの解説

```tsx:src/app/page.tsxのpatchTodo関数
const patchTodo = async (todo: todo) => {
    const res = await fetch(
        `${process.env.NEXT_PUBLIC_API_URL}/todo/${todo.id}`,
        {
            method: 'PATCH',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                isDone: !todo.isDone,
            }),
        },
    )
```

`body`には`isDone`を反転した(!をつけると反転)値を渡す。`false`の状態でチェックボックスをクリックしたら`true`に、逆に`true`の状態でクリックしたら`false`に。

```tsx
    const data = await res.json()
    setTodos(
        todos.map((todo) => {
            if (todo.id === data.id) {
                return data
            }
            return todo
        }),
    )
}
```

現在保存されている`todo`を`map`関数で一つずつ取り出す。

取り出した`todo`の`id`と、変更した`data`の`id`が一致していれば、受け取った`data`の方を返す。

一致していない場合は今まで通りの`todo`を返す。

# ３．更新してみよう

チェックボックスをクリックして変更されれば成功。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582046/9627a983-7a1c-5fd7-97a0-86b05db69ad5.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582046/778df779-5594-be4d-09c4-c5201e7b3ca8.png)


# おわりに

これでCRUD処理は全て実装完了！でも他にも色々つけ足してみよう。

# 参考にさせていただいた記事

https://zenn.dev/mostlove/articles/c9e6f1aa45ea09