sawata0324
@sawata0324 (Sawata)

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

Next.jsのダイナミックルーティングでフェッチできません

解決したいこと

Next.jsのダイナミックルーティングでどうしてもフェッチができません。何が問題でしょうか。

現在、Next.jsとRails APIサーバーを連携して開発を行っています。具体的には、Next.jsの動的ルートからRails APIサーバーにリクエストを送信してデータを取得し、ページに表示する機能を実装しています。
取り組み中の機能は一般的なタイトル付きのポスト投稿機能です。
これまでデータの取得自体は問題なくできていたのですが、ダイナミックルーティングを使用したところデータが取得できなくなってしまいました。

Railsログは反応なしの状態で、GPT4oに聞いてもAPIエンドポイントが存在していないのではという回答しか得られず、Web検索もAppRouterを使用しているので、これらのエラーについての記事が若干少なく、前に進められていません。
SSRを使用してデータを取得したいと思っています。(それ以前にデータフェッチができなくて四苦八苦しています)

githubのリンクは以下です。コードはfeatureブランチにプッシュしてあります。

自分で試したこと

・GPT4oに解決策を聞く
・docker-compose downからの再build,再up
・CORSの設定確認
・ログインクッキーの取得
・Railsログの確認(何も出力されない)
・console.logの設置と確認(何も出力されない)
・pram.idの確認(値は取れてきていることを確認)
・ThunderClientを使用してAPIエンドポイントにGetリクエストを送信(成功)
http://localhost:3000/api/v1/posts/72やhttp://localhost:3000/api/v1/postsを普通に開く(アクセス成功)
スクリーンショット 2024-05-26 3.03.36.png
・複数の動画教材によるダイナミックルーティングの復習

docker-compose logsは以下のようなログを出力しています。

frontend-1  | Error fetching post 71: 404
frontend-1  |  GET /api/v1/posts/71 404 in 110ms
frontend-1  |  GET /diarys/71 200 in 292ms
frontend-1  | Error fetching posts: 404
frontend-1  | Error: Failed to fetch posts: 404
frontend-1  |     at Object.generateStaticParams (webpack-internal:///(rsc)/./app/diarys/[id]/page.jsx:28:19)
frontend-1  |     at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
frontend-1  |     at async buildParams (/usr/src/app/node_modules/next/dist/build/utils.js:1026:40)
frontend-1  |     at async /usr/src/app/node_modules/next/dist/build/utils.js:1043:33
frontend-1  |     at async Object.loadStaticPaths (/usr/src/app/node_modules/next/dist/server/dev/static-paths-worker.js:47:16)
frontend-1  |  GET /api/v1/posts 404 in 50ms

post関連のディレクトリ構成は以下です。

my-app/
├── app/
   └── diarys/
       └── [id]/
            └── page.jsx
       └── components/
            └── PostInput.jsx
            └── PostView.jsx
       └── page.jsx
        

【開発環境の情報】

・MacOS
・Apple M1
・メモリ16GB
・ruby "3.3.0"
・rails "7.0.4.3"
・mySQL8.0.36(gem "mysql2","~> 0.5.6")
・"react": "^18", (画面遷移はAppRouterを使用しています)

該当するソースコード

TsunaRhythm2/front/my-app/app/diarys/[id]/page.jsx

// app/diarys/[id]/page.jsx

// 特定のポストの詳細情報を取得する関数
const fetchPostById = async (id) => {
  const res = await fetch(`http://localhost:3000/api/v1/posts/${id}`, {
    withCredentials: true,
  });
  if (!res.ok) {
    console.error(`Error fetching post ${id}: ${res.status}`);
    throw new Error(`Failed to fetch post: ${res.status}`);
  }
  const post = await res.json();
  return post;
};

// 動的パラメータを生成する関数
export async function generateStaticParams() {
  try {
    const res = await fetch("http://localhost:3000/api/v1/posts");
    if (!res.ok) {
      console.error(`Error fetching posts: ${res.status}`);
      throw new Error(`Failed to fetch posts: ${res.status}`);
    }
    const posts = await res.json();
    return posts.map((post) => ({ id: post.id.toString() }));
  } catch (error) {
    console.error(error);
    return []; // エラー発生時は空の配列を返す
  }
}

// ページコンポーネントの実装
export default async function PostPage({ params }) {
  try {
    const post = await fetchPostById(params.id);
    return (
      <div>
        <h1>{post.title}</h1>
        <p>{post.content}</p>
      </div>
    );
  } catch (error) {
    return (
      <div>
        <h1>Error: {error.message}</h1>
      </div>
    );
  }
}
routes.rb
Rails.application.routes.draw do
  get 'users/new'
  namespace :api, format: 'json' do
    namespace :v1 do
      resources :users do
        member do
          get :following, :followers
        end
      end
      get 'current_user', to: 'users#current_user_info'
      get 'posts_user', to: 'users#posts_user_info'
      resources :sessions, only: [:create]
      delete '/logout', to: 'sessions#destroy'
      # resources :account_activations, only: [:edit]
      resources :posts, only: %i[index show create update destroy]
      resources :password_resets,     only: %i[create edit update]
      resources :relationships,       only: %i[create destroy]
    end
  end
  get 'up' => 'rails/health#show', as: :rails_health_check
end
posts_controller.rb
module Api
  module V1
    class PostsController < ApplicationController
      include ActionController::Cookies
      include SessionsHelper
      before_action :logged_in_user, only: %i[create destroy update]
      before_action :correct_user, only: %i[destroy update]

      def index
        @posts = Post.all
        render json: @posts
      end

      def show
        @post = Post.find(params[:id])
        render json: @post
      end

      def create
        @post = current_user.posts.build(post_params)
        if @post.save
          message = [I18n.t('posts.create.flash.success')]
          render json: { status: 'success', message: message }, status: :created
        else
          message = @post.errors.full_messages
          render json: { status: 'failure', message: message }, status: :unprocessable_entity
        end
      end

      def update
        @post = Post.find(params[:id])
        if @post.update(post_params)
          render json: @post
        else
          render json: @post.errors, status: :unprocessable_entity
        end
      end

      def destroy
        @post = Post.find(params[:id])
        @post.destroy
        Rails.logger.info "ポストが削除されました。ユーザーID: #{current_user.id}"
        render json: { status: 'success', message: 'ポストが削除されました' }
      end

      private

      def post_params
        params.require(:post).permit(:title, :content )
      end

      def correct_user
        @post = current_user.posts.find_by(id: params[:id])
        render json: { error: '許可されていません' }, status: :forbidden if @post.nil?
      end

      def logged_in_user
        unless logged_in?
          Rails.logger.info '未ログインユーザーがログインが必要なページにアクセスしようとしました。'
          render json: { status: 'notLoggedIn', message: 'ログインしてください' }, status: :unauthorized
        end
      end
    end
  end
end

ちなみに以下のファイルではデータの取得ができています

PostView.jsx
import { useEffect, useState } from "react";
import axios from "axios";
import { format } from "date-fns";
import ja from "date-fns/locale/ja";
import Link from "next/link";

const PostView = ({ reload }) => {
  const [users, setUsers] = useState([]);
  const [posts, setPosts] = useState([]);
  const [editingPost, setEditingPost] = useState(null);
  const [newContent, setNewContent] = useState("");
  const [newTitle, setNewTitle] = useState("");

  useEffect(() => {
    const fetchUsersAndPosts = async () => {
      try {
        const [userRes, postRes] = await Promise.all([
          axios.get("http://localhost:3000/api/v1/posts_user", {
            withCredentials: true,
          }),
          axios.get("http://localhost:3000/api/v1/posts", {
            withCredentials: true,
          }),
        ]);

        const usersWithCurrentUserId = userRes.data.users.map((user) => ({
          ...user,
          current_user_id: userRes.data.current_user.id,
        }));
        setUsers(usersWithCurrentUserId);
        setPosts(postRes.data);
      } catch (error) {
        console.error("データの取得に失敗しました:", error);
      }
    };
    fetchUsersAndPosts();
  }, [reload]);

  const handleDelete = async (postId) => {
    try {
      await axios.delete(`http://localhost:3000/api/v1/posts/${postId}`, {
        withCredentials: true,
      });
      setPosts(posts.filter((post) => post.id !== postId));
    } catch (error) {
      console.error("ポストの削除に失敗しました:", error);
    }
  };

  const handleEditClick = (post) => {
    setEditingPost(post.id);
    setNewContent(post.content);
    setNewTitle(post.title);
  };

  const handleSave = async (postId) => {
    try {
      const response = await axios.patch(
        `http://localhost:3000/api/v1/posts/${postId}`,
        { title: newTitle, content: newContent },
        {
          withCredentials: true,
        }
      );
      setPosts(
        posts.map((post) =>
          post.id === postId
            ? {
                ...post,
                title: response.data.title,
                content: response.data.content,
              }
            : post
        )
      );
      setEditingPost(null);
    } catch (error) {
      console.error("ポストの編集に失敗しました:", error);
    }
  };

  if (!users.length || !posts.length) {
    return <div>Loading...</div>; // データがロード中の場合の表示
  }

  return (
    <div className="max-w-2xl mx-auto p-4">
      {posts.map((post) => {
        const user = users.find((user) => user.id === post.user_id);
        const formattedDate = format(
          new Date(post.created_at),
          "yyyy/M/d HH:mm",
          { locale: ja }
        );
        return (
          <div key={post.id} className="border-b border-gray-200 py-4">
            <div className="flex items-center mb-2">
              <div className="w-10 h-10 bg-gray-200 rounded-full mr-4 flex-shrink-0"></div>
              <div>
                <p className="text-lg font-semibold">{user.name}</p>
                <p className="text-sm text-gray-500">{formattedDate}</p>
              </div>
            </div>
            {editingPost === post.id ? (
              <div className="space-y-2">
                <label className="block">
                  <span className="text-gray-700">タイトル:</span>
                  <input
                    type="text"
                    value={newTitle}
                    onChange={(e) => setNewTitle(e.target.value)}
                    className="w-full px-3 py-2 border rounded-md"
                  />
                </label>
                <label className="block">
                  <span className="text-gray-700">投稿:</span>
                  <input
                    type="text"
                    value={newContent}
                    onChange={(e) => setNewContent(e.target.value)}
                    className="w-full px-3 py-2 border rounded-md"
                  />
                </label>
                <div className="space-x-2">
                  <button
                    onClick={() => handleSave(post.id)}
                    className="px-4 py-2 bg-blue-500 text-white rounded-md"
                  >
                    保存
                  </button>
                  <button
                    onClick={() => setEditingPost(null)}
                    className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md"
                  >
                    キャンセル
                  </button>
                </div>
              </div>
            ) : (
              <div className="ml-14">
                <Link href={`/diarys/${post.id}`}>
                  <p className="mb-2 text-gray-800 text-xl font-bold">
                    {post.title}
                  </p>
                </Link>
                <p className="mb-4 text-gray-600">
                  {post.content.length > 40
                    ? `${post.content.slice(0, 430)}...`
                    : post.content}
                </p>
                <div className="space-x-2">
                  {post.user_id === user?.current_user_id && (
                    <>
                      <button
                        onClick={() => handleEditClick(post)}
                        className="px-4 py-2 bg-yellow-500 text-white rounded-md"
                      >
                        編集
                      </button>
                      <button
                        onClick={() => handleDelete(post.id)}
                        className="px-4 py-2 bg-red-500 text-white rounded-md"
                      >
                        削除
                      </button>
                    </>
                  )}
                </div>
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
};

export default PostView;

どうかよろしくお願いいたします。必要な情報があればコメントお願いいたします。

0

1Answer

Error fetching post 71: 404とエラーにあることから404 not foundになっていると思います。

// 特定のポストの詳細情報を取得する関数
const fetchPostById = async (id) => {
  const res = await fetch(`http://localhost:3000/api/v1/posts/${id}`, {
  ...
  ..
  .

// 動的パラメータを生成する関数
export async function generateStaticParams() {
  try {
    const res = await fetch("http://localhost:3000/api/v1/posts");
    ...
    ..
    .

質問者様はhttp://localhost:3000/api/v1/postsにフェッチしようとしていますが、質問者様のファイル構成と当該リポジトリを拝見しましたところ、そもそもapiディレクトリが無いように見受けられます。

  • post関連のディレクトリ構成
my-app/
├── app/
   └── diarys/
       └── [id]/
            └── page.jsx
       └── components/
            └── PostInput.jsx
            └── PostView.jsx
       └── page.jsx
  • 当該リポジトリ

つまりNext.jsRoute Handlersのエンドポイントが無い状態です。

上記ページに記載されていますがRoute Handlersを使うにはapp/api/route.ts|jsでNext.js独自のAPIエンドポイントを設けることが一般的です。
そのため今回で言うと、app/api/v1/posts/route.ts|jsv1/postsのエンドポイントを設定してあげる必要があると思います。
(※詳細ページはapp/api/v1/posts/[id]/route.ts|jsで設定)

現状エンドポイントが無いため404が出たのだと思います。
これが解決の糸口になりますと幸いです。

追記:
PostView.jsxで何故フェッチ出来ているのかは分かりません。
恐れ入りますが他の有識者の方のご意見をお待ちください。
※もしかしてサーバーサイドのRails APIサーバーが立ち上がっているからローカルホストのAPIでそちらの方にデータフェッチしている、とかでしょうか?

0Like

Comments

  1. @sawata0324

    Questioner

    ご回答ありがとうございます。
    Route Handlersという単語は初めて聞きました。挑戦してみます。
    TsunaRhythm2リポジトリのback/app/controllers/api/v1/にてコントローラーをAPIで実装しており、dockerでbackとfrontのサーバーをそれぞれたてて使用しています。
    そのため

    ※もしかしてサーバーサイドのRails APIサーバーが立ち上がっているからローカルホストのAPIでそちらの方にデータフェッチしている、とかでしょうか?
    

    というのが当てはまると思います。

  2. @sawata0324 さん
    追記について、わざわざ教えていただきありがとうございます。
    無事にデータフェッチできることを祈っております。

Your answer might help someone💌