Next.js×RailsでEdit画面を作る

https://qiita.com/gabakugik/items/b0fcc09158d336e9e2a8
これの続きです。

ツリー構造
```
|-- app
|   |-- favicon.ico
|   |-- globals.css
|   |-- layout.tsx
|   |-- page.tsx
|   `-- todos
|-- components
|   |-- CreateTodoForm.tsx
|   |-- DeleteTodoButton.tsx
|   |-- EditTodoForm.tsx
|   |-- Todo.tsx
|   `-- Todos.tsx
```

todos以下のフォルダ構成
```
-- [id]
    |-- edit
    |   `-- page.tsx
    `-- page.tsx
```

```/backend/app/controllers/todos_controller.rb
class TodosController < ApplicationController
    
    def index
        # 日付が新しい順に10件まで取得する
        @todos = Todo.all.order(created_at: :desc).limit(10)
    
        render json: @todos
    
    end
    
    def show
    
        @todo = Todo.find(params[:id])

    render json: @todo
    end

    def create
          
        @todo = Todo.new(todo_params)
      
        if @todo.save
            render json: @todo, status: :created, location: @todo
          else
            render json: @todo.errors, status: :unprocessable_entity
          end
        end
      
        def update
          @todo = Todo.find(params[:id])
      
          if @todo.update(todo_params)
            render json: @todo
          else
            render json: @todo.errors, status: :unprocessable_entity
          end
        end
        
          # DELETE /todos/:id
          def destroy
            @todo = Todo.find(params[:id])
            @todo.destroy
          
          end
        
        def todo_params
          params.require(:todo).permit(:title, :content)
    end
end
```

```/frontend/components/EditTodoForm.tsx
"use client";

import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import axios from 'axios';

type EditTodoFormProps = {
  id: number;
};

// Todo を編集するフォーム
const EditTodoForm = ({ id }: EditTodoFormProps) => {
  const router = useRouter();

  // フォームの入力値を管理するstate
  const [title, setTitle] = useState('');

  // フォームの入力値を管理するstate
  const [content, setContent] = useState('');

  // idが変更されたら(=Todo編集ページを開いたら)、Todoを取得してフォームの初期値を設定する
  useEffect(() => {
    // idが存在しない場合は、処理を中断する
    const fetchTodo = async () => {
      try {
        // idを元にTodoを取得する
        const res = await axios.get(`http://localhost:3000/todos/${id}`);

        // フォームの初期値を設定する
        const { title, content } = res.data;
        setTitle(title);
        setContent(content);
      } catch (err) {
        console.log(err);
      }
    };

    // idが存在する場合は、Todoを取得する
    if (id) {
      fetchTodo();
    }
  }, [id]);

  // フォームの入力値を更新する関数
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      // APIを呼び出して、Todoを更新する
      await axios.put(`http://localhost:3000/todos/${id}`, { todo: { title, content } });

      // Todoの更新に成功したら、Todo詳細ページに遷移する
      router.push(`/todos/${id}`);
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div className="space-y-6 py-16">
      <form
        onSubmit={handleSubmit}
        className="space-y-6"
      >
        <label className="block text-xl font-bold text-gray-700">Edit Todo</label>
        <input
          type="text"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="タイトル"
          className="block w-full py-2 pl-3 pr-4 text-base border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-gray-500 focus:border-gray-500 sm:text-sm"
        />
        <textarea
          value={content}
          onChange={(e) => setContent(e.target.value)}
          placeholder="本文"
          className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-gray-500 focus:border-gray-500 sm:text-sm"
        />
        <button
          type="submit"
          className="mt-3 ml-auto flex justify-center py-2 px-8 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
        >
          Update
        </button>
      </form>
    </div>
  );
};

export default EditTodoForm;
```

```/frontend/app/todos/[id]/edit/page.tsx
"use client";

import { useParams } from 'next/navigation';
import Link from 'next/link';
import EditTodoForm from '@/components/EditTodoForm';

const EditTodoPage = () => {
  // Use useParams to retrieve the id from the URL
  const { id } = useParams<{ id: string }>();

  // Display loading state until id is available
  if (!id) {
    return <div>Loading...</div>;
  }

  return (
    <div className="flex justify-center items-center">
      <div className="flex flex-col w-3/4 max-w-lg">
        <EditTodoForm id={parseInt(id)} />
        <Link
          href="/"
          className="ml-auto font-medium text-blue-600 hover:bg-blue-300 focus:outline-none"
        >
          Back
        </Link>
      </div>
    </div>
  );
};

export default EditTodoPage;
```

```/frontend/app/todos/[id]/page.tsx
"use client";

import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import useSWR from 'swr';
import Link from 'next/link';
import Todo from '@/components/Todo';
import { TodoType } from '@/types/Todo';
import axios from 'axios';
import DeleteTodoButton from '@/components/DeleteTodoButton';

// Fetcher function for SWR
const fetcher = (url: string) => axios.get(url).then(res => res.data);

// Todo詳細ページを表示するコンポーネント
const TodoDetail = ({ params }: { params: { id: string } }) => {
  // ルーティング情報を取得する
  const router = useRouter();
  const { id } = params;

  // SWRを使ってデータを取得する
  const { data: todo, error } = useSWR<TodoType>(id ? `http://localhost:3000/todos/${id}` : null, fetcher);

  // エラーが発生した場合の処理
  if (error) return <div>Failed to load</div>;

  // Todoを取得中の場合は「Loading...」を表示する
  if (!todo) return <div>Loading...</div>;

  return (
    <div className="flex justify-center items-center">
      <div className="flex flex-col space-y-6 w-3/4 max-w-lg pt-10">
        <label className="block text-xl font-bold text-gray-700">Todo</label>
        <Todo todo={todo} />
        <div className="flex justify-end">
          <Link
            href={`/todos/${id}/edit`}
            className="mt-auto font-medium text-blue-600 hover:bg-blue-300 focus:outline-none mr-12"
          >
            Edit
          </Link>
          <Link
            href="/"
            className="mt-auto font-medium text-blue-600 hover:bg-blue-300 focus:outline-none"
          >
            Back
          </Link>
        </div>
        {/* 削除ボタンコンポーネントを追加 */}
        <DeleteTodoButton id={todo.id} />
      </div>
    </div>
  );
};


export default TodoDetail;
```

これでEdit画面は作れたはずです。
最後に削除機能を作ります。