gabakugik
@gabakugik (GABAKU GIK)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

Editがうまくいかない

解決したいこと

https://qiita.com/gabakugik/items/b0fcc09158d336e9e2a8
の続きです。
Editボタンを押すとLoadingになってしまう。

Nexts.jsとRuby on RailsでCRUDWebアプリをつくっています。
Editボタンを押すと飛ぶようになりましたが更新ができません。
解決方法を教えて下さい。

できました。ありがとうございます。
下記の通りにやったらできました。

ツリー構造
スクリーンショット 2024-08-18 014111.png

該当するソースコード

/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

        def todo_params
          params.require(:todo).permit(:title, :content)
    end
end
/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';

// 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/` : 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>
      </div>
    </div>
  );
};

export default TodoDetail;
/frontend/components/EditTodoForm.tsx
"use client"

import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
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;

自分で試したこと

環境がだめになったしまったので、一からもう一度作り直した。
appがひとつ少なくなった。

ここ参考になるよとかここのコードみてごらんとかヒントだけでもいいのでよろしくお願いします。

1

2Answer

前のスレッド https://qiita.com/gabakugik/questions/4959e67e9253d7cfe994 とは違う話なのでしょうか?

Editボタンを押したら404のnot foundがでるのをなんとかしたい。

404 応答が返ってくるということは、名前解決ができてクライアントからの要求はサーバーに届き、サーバーで url に指定されているリソースを探したが見つからなかったということです。原因は、自分がここのような Q&A サイトで見た限りですが、ほとんどは指定した url が間違っているということでした。

間違いには、単純なスペルミス、相対パスの使い方の間違い、サーバーでのルーティングの間違い、要求先サーバーを間違えたなどがあります。

まずは、Edit ボタンを押すとどういう要求がサーバーに送信されるかを、質問者さんが調べるところから始めてください。

0Like

Comments

  1. まずは、Edit ボタンを押すと404エラーが送信されます。

    話が通じてないようです。基本的なところが分かってないということはありませんか? 404 Not Found というのは、上の回答に書いたとおり Web サーバーが返す応答です。「404エラーが送信されます」なんてことはあり得ません。

Ruby詳しく無いのですが、indexは表示できてるので、ルーティングの設定でしょうか。

https://musclecoding.com/next-js-rails-todo-tutorial/
を見ると、
‘config/routes.rb‘ でeditを除外しているように見えます。
‘’’
Rails.application.routes.draw do
resources :todos, except: [:new, :edit]
end
‘’’
これをこのまま使っているのであれば、exceptからeditを削除したら表示されるかもです🤔

0Like

Comments

  1. @gabakugik

    Questioner

    Rails.application.routes.draw do
    resources :todos
    end
    やってみたのですが、表示されませんでした。

  2. @gabakugik

    Questioner

    動きました。画面がでてきました。ありがとうございます。
    ただ更新作業ができません。
    消したり増やしたりできないです。

  3. def updateとdef todo_paramsがdef createにネストされているからではないでしょうか?

  4. @gabakugik

    Questioner

    すいません。わかんないです。
    どう書けばいいんでしょうか?
    試してみたいです。

  5. インデント違うだけかも…🤔
    更新はどんなエラーになるのでしょうか?(ブラウザ(クライアント)でエラーになるのか、サーバーでエラーになるのか)
     
    以下のように原因を切り分けて調べていくと良いかと思います。
    ・curlでRubyサーバーに直接Putリクエストを送ってみる
     ・成功する場合、Frontの問題(リクエストが遅れてないか、パラメータ不正など
     ・失敗する場合、Backendの問題(エラーログを確認して修正

  6. @gabakugik

    Questioner

    curlでcurl -X PUT http://localhost:3000/todos/7 -d 'todo[title]=test333&todo[content]=com'
    を打ちましたところ無事更新されました。
    フロントエンドの問題みたいです。

  7. では、Chromeのデベロッパーツールやconsoleログを使って、リクエストが正しく送れているかを確認していくと原因がわかるかと👌
    リクエストが送信できているのであれば、ボディの形式やリクエストパス、リクエストメソッドなどが違っていると思われるのでそれを修正。
    そもそも、リクエストがされていないのであれば、consoleログやデバッグで処理を追う

    参考:
    https://developer.chrome.com/docs/devtools/network/reference?hl=ja

  8. 貼っていただいた、localhost.har をみると GETリクエストしか送られてなさそうですね。
    ブラウザの Developer Tools でリクエストの内容を確認できるので、更新ボタンを押した際にリクエストが送られているかを確認していただいた方が良いかと思います。
    ボタン押下時に何も変化がなければ、リクエストが送られていません。

    その場合、ボタン押下時にブラウザのコンソールにエラーが出力されていないか確認してください。

    エラーが発生していない場合、以下のようにデバッグログの出力を追加して、処理がどこまで通っているか確認すると良いかと思います。
    console.log("デバッグ用のログ出力")

    参考 ※Chromeの場合
    https://btj0.com/blog/web/chrome-post/

    公式 ※Chromeの場合
    https://developer.chrome.com/docs/devtools/network?hl=ja

  9. @gabakugik

    Questioner

    すいません。
    どうconsole.log("デバッグ用のログ出力")でデバックしたらいいかわかんないです。

  10. 例えば、 EditTodoForm.tsx の送信処理の中に仕込むとかですね。
    引数に指定した文字列がブラウザのコンソールログに出力されます。
    https://www.w3schools.com/jsref/met_console_log.asp

    // ~ 前略 ~
    
      // フォームの入力値を更新する関数
      const handleSubmit = async (e: React.FormEvent) => {
        // ====================
        // ログ出力追加 ①
        // ====================
        console.log("Editボタンがクリックされた");
        e.preventDefault();
        try {
          // APIを呼び出して、Todoを更新する
          await axios.put(`http://localhost:3000/todos/${id}`, { todo: { title, content } });
    
          // ====================
          // ログ出力追加 ②
          // ====================
          console.log("axios.putが実行された");
    
          // Todoの更新に成功したら、Todo詳細ページに遷移する
          router.push(`/todos/${id}`);
        } catch (error) {
          console.error(error);
        }
      };
    
    // ~ 後略 ~
    
  11. またツリー構造に書きますが、page.tsxが2つ必要になりました。一つにまとめたい。

    page.tsx は両方必要です。
    参考:https://www.tohoho-web.com/ex/nextjs.html#app-router-and-pages-router

    /frontend/app/app/todos/[id]/edit/page.tsx

    /frontend/app/app/todos/[id]/page.tsx
    と同じ内容になっているように見えますが、これだと todos/[id]/edit でも詳細のコンポーネントが表示されます。

    編集画面を表示するには、

    1. /frontend/app/app/todos/[id]/edit/page.tsxを削除する
    2. /frontend/app/app/todos/[id]/edit/edit.tsx/frontend/app/app/todos/[id]/edit/page.tsx にリネームする

    としてください。

  12. @gabakugik

    Questioner

    わかりました。やってみます。

  13. @gabakugik

    Questioner

    やってみたんですが、だめでした。
    環境が壊れてしまい再構築したんですが.....
    Editボタンを押すとLoading画面になってしまいます。

  14. それはデグレかと思いますが、リネーム以外の箇所を変えてしまってませんか?
    (404であれば、コード/ブラウザ上で遷移先のURLが合っているかはよく確認してください)

  15. @gabakugik

    Questioner

    404はなくなりました。ありがとうございます。
    ただLoading中はなくなりません。
    編集できません。
    すいません。全ソース載せるので見てもらうことは可能でしょうか?
    確認したいんですがフロントのフォルダ構成ってこれであっていますか?
    間違えた構成になってないですか?

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

    となっているので、編集画面でLoadingになるのであれば、idが取れていません。
    パスパラメータの取得には、useSearchParams ではなく、useParams を利用してください。

    すいません。全ソース載せるので見てもらうことは可能でしょうか?

    はい、GitHubとかに上げていただけると...

    確認したいんですがフロントのフォルダ構成ってこれであっていますか?
    間違えた構成になってないですか?

    載せていただいている app配下の構成はあっているように見えます。

  17. ReactもRailsもほぼ知見はないのですが事象を聞く限り、idが上手く取得できていないことが原因なのかな?と仮説を立てました。

    解消するかどうかは分かりませんが解決案を以下に記載します。

    /frontend/app/todos/[id]/edit/page.tsx
    "use client";
    
    import { useRouter } from 'next/router';
    import Link from 'next/link';
    import EditTodoForm from '@/components/EditTodoForm';
    
    const EditTodoPage = () => {
      // Use Router to retrieve the id from the URL
      const router = useRouter();
    
      if (!router.isReady) {
        return <div>Loading...</div>;
      }
    
      const { id } = router.query;  
    
      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;
    

    上記のようにuseRouterを使用することで
    URLの/todos/123/editから「123」のid部分の取得が出来ると思います。
    ただし、初期レンダリング時には空で処理が進んでしまうため
    router.isReadyを使用して空の間はLoading...を表示し、
    値が入った時点で以降の処理を実行するような書き方に変更したらいかがでしょうか。
    以降の処理では記載の通りrouter.queryを使用してidのみ分割代入で取得が出来ると思います。

  18. 原因についてはご推察の通りかと思います。

    Next のルーティング機能に App Router と Pages Router (古いもの) があり、
    本記事のソースでは App Router の方を利用しています。
    App Routerだと next/router が使えません >< 💦
    そのため、本ケースでは、useSearchParams ではなく、 useParams を使うようにすれば動くかと思います。

    import { useParams } from 'next/navigation';
    
    // 省略
    
    const { id } = useParams()
    

    参考:

  19. @grhg
    なるほど。
    バージョン関連の考慮が不足していたのでそのあたりも気にかけるべきだと改めて勉強になりました。ありがとうございます!

    後はハンプティさんの結果待ちですね♪♪

  20. @gabakugik

    Questioner

    /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;
    
    
    

    これでいけました。ありがとうございます。

  21. 良かったですー :thumbsup:

  22. @gabakugik

    Questioner

    本当にありがとうございました。
    助かりました。

Your answer might help someone💌