【7. 投稿機能を作る】ReactとRails APIで投稿サービスを作る(認証なし)

この記事の1章から5章まではRails、6章から8章まではReactの解説です。

※当記事で作成する投稿サービスの「全体の手順」は「【手順解説】ReactとRails APIで投稿サービスを作る(認証なし)」を参考にしてください。

目次 非表示

1. [Rails]データベースにポストデータを追加する

1-1. ポストデータモデルの確認

下記ポストデータモデルを参考に、MySQLにポストデータ用のテーブルとカラムを作成します。

※Ruby on Railsではidが自動的に主キーとして設定されるため、idは省略可能です。また、created_at、updated_atも省略可能です。

1-2. 「rails db:create」コマンドで、データベースを作成する

下記コマンドを実行し、データベースを作成します。

$ docker-compose exec app rails db:create

すでに「【初学者向け】DockerでRails APIとMySQLの環境構築をする(M1 Mac)」記事の「10. データベースを作成する」でデータベースを作成している場合は、「rails db:create」を実行する必要はありません。

データベースをすでに作成している状態で「rails db:create」を実行すると、すでに作成済みです、と表示が出ます。

1-3. [Postモデル]マイグレーションファイルを作成/編集

※「4-1. 「rails g model」コマンドでポストモデルを作成する」で解説するrials g modelコマンドを使用すれば、Modelとマイグレーションファイルを同時に自動生成してくれますが、今回は手順が分かりやすいように、別々に生成コマンドを実行します。

下記コマンドを実行し、マイグレーションファイル(データベースへの変更が記載されたファイル)を作成します。

$ docker-compose exec app rails generate migration CreatePosts title:string content:text username:string

上記コマンドを実行すると、下記のマイグレーションファイルが作成されます。

※下記[7.0]の部分は、[7.1]の場合もあります。Railsのgemのバージョンは「gem “rails”, “~> 7.1.3″」で固定されているため、マイグレーションの作成日時によっては、[7.1]になります。2024年2月17日時点では[7.1]でした。

【db/migrate/20230714035442_create_posts.rb】

class CreatePosts < ActiveRecord::Migration[7.0]
  def change
    create_table :posts do |t|
      t.string :title
      t.text :content
      t.string :username

      t.timestamps
    end
  end
end

下記のようにマイグレーションファイルを修正します。

【db/migrate/20230714035442_create_posts.rb】

class CreatePosts < ActiveRecord::Migration[7.0]
  def change
    create_table :posts do |t|
      t.string :title, limit: 255, null: false
      t.text :content, limit: 5000
      t.string :username, null: false

      t.timestamps
    end
  end
end

1-4. 「rails db:migrate」コマンドで、マイグレーションファイルの内容をデータベースに反映させる

下記コマンドを実行し、マイグレーションファイル(データベースへの変更が記載されたファイル)の内容をデータベースに反映させます。

$ docker-compose exec app rails db:migrate
【db/schema.rb】
ActiveRecord::Schema[7.0].define(version: 2023_07_14_035442) do
  create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.string "title", null: false
    t.text "content"
    t.string "username", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

2. [Rails]コントローラを作成する

コントローラの役割は、アプリケーションのビジネスロジックやフロントエンドとバックエンドの間のやり取りを制御することです。

例えば、ユーザーのリクエストを受け取り、適切なデータベース操作を行ったり、ビューにデータを渡したり、APIのレスポンスを返したりする処理を担当します。

※Railsでは、rails g controllerコマンドを実行しcontrollerファイルを作成することで、「Viewフォルダの自動生成」や「ルーティングの自動設定」等を同時に行ってくれます。

※RailsをAPIモードで構築している場合は、「Viewフォルダの自動生成」は行われません。

2-1. Controllerをモジュール化するためのテンプレートを作成

「lib/templates/rails/controller/」配下に「controller.rb」テンプレートを配置します。rails g controllerコマンドを実行したとき、テンプレートを自動的に読み込んでControllerを作成してくれます。

※下記コードでは、APIをV1という名前のモジュールとして提供することで、後々のバージョン変更時に、簡単に別のV2という新しいバージョンを作成できるようにしています。

【lib/templates/rails/controller/controller.rb】
module V1
  class <%= class_name.gsub('V1::', '') %>Controller < ApiController
  end
end

2-2. ApiControllerを作成

ApiControllerの役割は、RailsAPIのすべてのコントローラーが共通して利用する基本的な機能や設定を提供することです。

例えば、認証や認可、エラーハンドリングの設定などをApiControllerに記述します。

今回は、moduleとclassだけ用意します。

【app/controllers/v1/api_controller.rb】
module V1
  class ApiController < ApplicationController
  end
end

2-3. 「rails g controller」コマンドでPostsコントローラを作成する

2-1. Controllerをモジュール化するためのテンプレートを作成」で作成したテンプレートを基に、rails g controllerコマンドを実行し、コントローラファイルを作成します。

Controllerファイルと同時に、ルーティングファイル(route.rb)も自動生成されます。

$ docker-compose exec app rails generate controller v1/posts

もし自動的に読み込まれない場合は、$ docker-compose exec app rails generate controller v1/posts --template=lib/templates/rails/controller/controller.rbのように、明示的にテンプレートを指定してください。

2-4. PostsコントローラファイルにCRUD処理を追加

【app/controllers/v1/posts_controller.rb】

module V1
  class PostsController < ApiController
    def index
      posts = Post.all
      render json: posts
    end

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

    def create
      post = Post.new(post_params)

      if post.save
        render json: post, status: :created
      else
        render json: { errors: post.errors.full_messages }, status: :unprocessable_entity
      end
    end

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

    def destroy
      post = Post.find(params[:id])
      post.destroy
      render json: {}, status: :no_content
    end

    private

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

3. [Rails]ルーティングを設定する

2. [Rails]コントローラを作成する」で自動生成された「route.rb」を編集します。

Rails.application.routes.draw do
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
  namespace :v1 do
    # 基本的なメソッドを一括で表したい場合
    resources :posts

    # 特定のメソッドを使用しない場合
    # resources :posts, only: [:index, :show, :create]

    # 個別にメソッドを定義したい場合
    # get '/posts', to: 'posts#index'
    # post '/posts', to: 'posts#create'
    # get '/posts/:id', to: 'posts#show'
    # patch '/posts/:id', to: 'posts#update'
    # delete '/posts/:id', to: 'posts#destroy'
  end
end

resources :postsは、index, create, show, update, destoryメソッドそれぞれにルーティングされます。

「config/routes.rb」にルーティングを記載することで、例えば「get ‘/posts’, to: ‘posts#index’」の「’/posts’」のURLにアクセスがあった場合、「app/controllers/v1/posts_controller.rb」内のindexメソッドの処理を実行することができます。

ルーティングに関する詳しい解説は、こちら(準備中)を参考にしてください。

Railsでは、コントローラとルーティングが密結合していることにより、素早いアプリケーション開発を実現できます。

4. [Rails]モデルを作成する

モデルの役割は、データベースとのやり取りやビジネスロジックの実装などを担当します。

例えば、データベースのテーブルとマッピングし、データの作成や読み取り、更新、削除といった処理を担当します。

また、モデルはデータのバリデーションやDBのテーブル同士のリレーション(アソシエーション)の定義なども行います。

4-1. 「rails g model」コマンドでモデルを作成する

※「1-3. [Postモデル]マイグレーションファイルを作成/編集」において、すでにマイグレーションファイルを作成しているため、今回はrails g modelコマンドに--skipオプションをつけ、マイグレーションファイル自動生成のコンフリクトを防止します。

まずはrails g modelコマンドを実行し、モデルファイルを作成します。

$ docker-compose exec app rails generate model Post --skip

4-2. モデルファイルにバリデーションを追加

今回の投稿データは、titleとusername(作成者)を必須とし、content(内容)は必須ではないことにします。

class Post < ApplicationRecord
  validates :title, presence: true
  validates :username, presence: true
end

※今回は、基本的なバリデーションのみ実装します。Railsのバリデーションについてより詳しく知りたい方は、こちら(準備中)を参考にしてください。

※今回は、アソシエーションについては詳しく解説しません。アソシエーションについて詳しく知りたい方は、こちら(準備中)を参考にしてください。

5. [Rails]rack-corsを用いて、別のURLとのAPI通信を可能にする

5-1. rack-corsをプロジェクトにinstallする

アプリケーションのバックエンドを作成するとき、セキュリティの面から、バックエンドと通信できるURLを制御するのが基本です。

rack-corsとは、APIで使用されているURLとは別のURLとの通信を行う設定をするgemです。

例えば、Rails APIで「http://localhost:3000」、フロントエンドで「http://localhost:3001」のURLを使用しているとき、Rails API側でフロントエンドのURLを設定することで、「http://localhost:3001」との通信ができるようになります。

Railsの雛形を作成する際、rails newコマンドで自動生成されたGemfileでは、rack-corsはコメントアウトされています。

このrack-corsのコメントアウトを解除して、bundle installを実行し、プロジェクト内でrack-corsを使用できるようにします。

【[更新前]Gemfile】

・・・省略・・・

# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
# gem "rack-cors"

・・・省略・・・
【[更新後]Gemfile】

・・・省略・・・

# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
gem "rack-cors"

・・・省略・・・
$ docker-compose exec app bundle install(Dockerコンテナを起動している時のコマンド)
$ docker-compose run app bundle install(Dockerコンテナを起動していない時のコマンド)

5-2. rack-corsの設定で、「http://localhost:3001」との通信を許可する

rack-corsの設定を行う、「config/initializers/cors.rb」はデフォルトで下記のように設定がコメントアウトされています。

【[更新前]config/initializers/cors.rb】
# Rails.application.config.middleware.insert_before 0, Rack::Cors do
#   allow do
#     origins "example.com"
#
#     resource "*",
#       headers: :any,
#       methods: [:get, :post, :put, :patch, :delete, :options, :head]
#   end
# end

以下の手順に沿って、rack-corsの設定をオンにします。

  1. コードのコメントアウトを解除
  2. 「origins “example.com”」を「origins ENV[‘FRONTEND_ORIGIN’]」に変更
  3. 「.env」ファイルに、「FRONTEND_ORIGIN」の変数と値をセットする。

「config/initializers/cors.rb」で「origins ENV[‘FRONTEND_ORIGIN’]」をallowにすることによって、APIがフロントエンドのURLからのリクエストを許可します。

【[更新後]config/initializers/cors.rb】
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins ENV['FRONTEND_ORIGIN']

    resource "*",
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

【初学者向け】DockerでRails APIとMySQLの環境構築をする(M1 Mac)」で作成した「.env」ファイルに、「FRONTEND_ORIGIN」の変数をセットしてください。

※すでにセットしている場合はスキップして大丈夫です。

【.env】
DB_USER=root
DB_PASSWORD=12345678
DB_HOST=db
DB_PORT=3306

# 以下をセット
FRONTEND_ORIGIN=http://localhost:3001

そして最後に、コンテナを再ビルドします。以下の手順を実行してください。

  1. docker-compose downを実行し、コンテナを終了する
  2. docker-compose buildを実行し、ビルドする
  3. docker-compose up -dを実行し、コンテナを立ち上げる

Rails側の設定はこれで終わりです。次はReactの設定に移ります。

6. [React]API通信用関数を作成する

まずは、Rails APIとの通信を行う、API通信用関数を作成します。

今回は、axiosを用いて通信を簡単にします。

6-1. axios、camelcase-keys、snakecase-keysをプロジェクトに追加

axiosはAPI通信を簡略化するメソッドを提供してくれます。

基本的にReactではキャメルケース、Railsではスネークケースを採用しているため、そのまま値を送信し合っていると、値の管理が煩雑になりがちです。

そこで、リクエストやデータの送受信時に値の形式を変換し揃えるため、camelcase-keysとsnakecase-keysを用います。

※キャメルケース等について詳しく知りたい方は、こちらを参考にしてください。

yarn addコマンドを実行し、プロジェクトにそれぞれのpackageを導入してください。

$ docker-compose exec front yarn add axios camelcase-keys snakecase-keys

6-2. 環境情報を.envに設定する

プロジェクトのルートに「.env」ファイルを作成し、「REACT_APP_API_URL」の変数をセットしてください。

REACT_APP_API_URL="http://localhost:3000/v1"

6-3. API通信用関数を定義する

「src/infra/api.js」ファイルを用意し、そこに下記のように関数を作成します。

【src/infra/api.js】

import axios from "axios";
import camelcaseKeys from "camelcase-keys";
import snakecaseKeys from "snakecase-keys";

axios.defaults.baseURL = process.env.REACT_APP_API_URL;
axios.defaults.headers.common["Content-Type"] = "application/json";


// posts
export const postPost = async (params) =>
  axios.post("/posts", snakecaseKeys(params));

export const getPost = async (params) =>
  axios({
    method: "get",
    url: `/posts/${params}`,
  }).then((response) => camelcaseKeys(response, { deep: true }));

export const updatePost = async (params, data) =>
  axios({
    method: "put",
    url: `/posts/${params}`,
    data: snakecaseKeys(data),
  }).then((response) => camelcaseKeys(response, { deep: true }));

export const deletePost = async (params) =>
  axios({
    method: "delete",
    url: `/posts/${params}`,
  });

export const getPosts = async () =>
  axios({
    method: "get",
    url: "/posts",
  }).then((response) => camelcaseKeys(response, { deep: true }));

今回はこれらのAPI通信用関数を、機能別のページで呼び出して使用します。

7. [React]ルーティングを設定する

Reactでルーティングを設定するには、ルーティング用のpackageをinstallする必要があります。

7-1. react-router-domをinstallする

Reactでルーティングを設定する際のデファクトスタンダードである「react-router-dom」をプロジェクトにinstallします。

$ docker-compose exec front yarn add react-router-dom

7-2. App.jsにルーティングを設定する

App.jsにルーティングを一括で定義します。

今回は、ブラウザから「localhost: 30001(ルートURL)」にアクセスしたときに、「<Posts />」ページ(投稿一覧)が表示されるように設定します。

【src/App.js】

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
// Posts
import { Post } from "./pages/posts/Post";
import { PostCreate } from "./pages/posts/PostCreate";
import { PostEdit } from "./pages/posts/PostEdit";
import { Posts } from "./pages/posts/Posts";

const App = () => {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Posts />} />
        <Route
          path="/posts/:id"
          element={<Post />}
        />
        <Route
          path="/posts/create"
          element={<PostCreate />}
        />
        <Route
          path="/posts/:id/edit"
          element={<PostEdit />}
        />
      </Routes>
    </Router>
  );
};

export default App;

※次にUIの構築を行いますが、1ページずつ表示を確認しながら学習を進めたい方は、App.jsで定義したルーティングにおいて、表示していないページのルーティングをコメントアウトしながら進めてください。

8. [React]UI(ページ)を構築する

今回はコンポーネントを分けずに、機能別にそれぞれ1枚のページでUIを作成します。Reactコンポーネントを分割する方法については、こちら(準備中)を参考にしてください。

最終的なフォルダ構成は以下になります。

※今回は「API通信用カスタムHooks」等を作成せずに、1つのUIの中でstateやAPI通信用の関数定義、エラーハンドリングを行います。実際の開発ではこれらを別々のファイルに分けることで、開発の効率をアップさせます。

※「API通信用カスタムHooksの作り方」は、こちら(準備中)を参考にしてください。

8-1. 投稿作成ページを作成

【src/components/posts/PostCreate.jsx】

import { useState } from 'react';
import { useNavigate } from "react-router-dom";
import { postPost } from "../../infra/api";

export const PostCreate = () => {
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');
  const [username, setUserName] = useState('');
  const navigateToPosts = useNavigate();

  const handleTitleChange = (e) => {
    setTitle(e.target.value);
  };

  const handleContentChange = (e) => {
    setContent(e.target.value);
  };

  const handleUserNameChange = (e) => {
    setUserName(e.target.value);
  };

  const postPostFunc = async (post) => {
    try {
      await postPost(post);
      navigateToPosts("/");
    } catch (error) {
      console.log(error);
    }
  };

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

    const newPost = {
      title: title,
      content: content,
      username: username,
    };

    await postPostFunc(newPost);

    setTitle('');
    setContent('');
    setUserName('');
  };

  return (
    <div>
      <h2>投稿作成</h2>
      <form onSubmit={handleSubmit}>
        <div>
          <label>Title:</label>
          <input type="text" value={title} onChange={handleTitleChange} required />
        </div>
        <div>
          <label>Content:</label>
          <input type="text" value={content} onChange={handleContentChange} required />
        </div>
        {/* 実際の開発では、現在ログインしているユーザーのidを裏でサーバーに送ったりします。 */}
        <div>
          <label>UserName:</label>
          <input type="text" value={username} onChange={handleUserNameChange} required />
        </div>
        <button type="submit">作成</button>
      </form>
    </div>
  );
};

8-2. 投稿一覧ページを作成

【src/components/posts/Posts.jsx】

import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { getPosts } from "../../infra/api";

export const Posts = () => {
  const navigate = useNavigate();
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchPostData = async () => {
    try {
      const response = await getPosts();
      const postData = response.data;
      setPosts(postData);
      setLoading(false);
    } catch (error) {
      setError(error);
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchPostData();
  }, []);

  return (
    <div>
      <h2>投稿一覧</h2>
      <button onClick={() => navigate(`/posts/create`)}>投稿作成</button>
      {loading ? (
        <p>Loading...</p>
      ) : error ? (
        <p>Error: {error.message}</p>
      ) : posts.length > 0 ? (
        <>
          {posts.map((post) => (
            <div key={post.id}>
              <p>
                Title: <Link to={`/posts/${post.id}`}>{post.title}</Link>
              </p>
              <p>Content: {post.content}</p>
              <p>UserName: {post.username}</p>
            </div>
          ))}
        </>
      ) : (
        <p>投稿がありません。</p>
      )}
    </div>
  );
};

8-3. 投稿詳細ページを作成

【src/components/posts/Post.jsx】

import { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { getPost, deletePost } from "../../infra/api";

export const Post = () => {
  const { id } = useParams();
  const navigate = useNavigate();
  const [post, setPost] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchPostData = async () => {
    try {
      const response = await getPost(id);
      const postData = response.data;
      setPost(postData);
      setLoading(false);
    } catch (error) {
      setError(error);
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchPostData();
  }, [id]);

  return (
    <div>
      <h2>投稿詳細</h2>
      {loading ? (
        <p>Loading...</p>
      ) : error ? (
        <p>Error: {error.message}</p>
      ) : (
        <div>
          <p>Title: {post.title}</p>
          <p>Content: {post.content}</p>
          <p>UserName: {post.username}</p>
        </div>
      )}
    </div>
  );
};

8-4. 投稿編集ページを作成

【src/components/posts/PostEdit.jsx】

import { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { getPost, updatePost } from "../../infra/api";

export const PostEdit = () => {
  const { id } = useParams();
  const navigate = useNavigate();
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [username, setUserName] = useState("");
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchPostData = async () => {
    try {
      const response = await getPost(id);
      const postData = response.data;
      setTitle(postData.title);
      setContent(postData.content);
      setUserName(postData.username);
      setLoading(false);
    } catch (error) {
      setError(error);
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchPostData();
  }, [id]);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    try {
      const updatedPostData = {
        title,
        content,
        username,
      };

      await updatePost(id, updatedPostData);
      navigate(`/posts/${id}`);
    } catch (error) {
      setError(error);
      setLoading(false);
    }
  };

  return (
    <div>
      <h2>投稿編集</h2>
      {loading ? (
        <p>Loading...</p>
      ) : error ? (
        <p>Error: {error.message}</p>
      ) : (
        <form onSubmit={handleSubmit}>
          <div>
            <label>Title:</label>
            <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} required />
          </div>
          <div>
            <label>Content:</label>
            <textarea value={content} onChange={(e) => setContent(e.target.value)} required />
          </div>
          <div>
            <label>UserName:</label>
            <input type="text" value={username} onChange={(e) => setUserName(e.target.value)} required />
          </div>
          <button type="submit">更新</button>
        </form>
      )}
    </div>
  );
};

8-5. 投稿詳細ページに、投稿編集ボタンと投稿削除ボタンを追加

8-3. 投稿詳細ページを作成」で作成した投稿詳細ページに、「投稿編集ボタン(投稿編集ページへのリンク)」と「投稿削除ボタン」を追加します。

今回は、見た目のわかりやすさのため、「投稿編集ページへのリンク」をボタンの形式にしています。

【src/components/posts/Post.jsx】

import { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
# 投稿削除用のdeletePost関数を呼び出す
import { getPost, deletePost } from "../../infra/api";

export const Post = () => {
  const { id } = useParams();
  const navigate = useNavigate();
  const [post, setPost] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

・・・省略・・・

  # 投稿削除用の関数を追加
  const handleDelete = async () => {
    setLoading(true);
    try {
      await deletePost(id);
      navigate("/");
    } catch (error) {
      setError(error);
      setLoading(false);
    }
  };

  return (
    <div>
      <h2>投稿詳細</h2>
      {loading ? (
        <p>Loading...</p>
      ) : error ? (
        <p>Error: {error.message}</p>
      ) : post ? (
        <div>
          <p>Post Title: {post.title}</p>
          <p>Post Content: {post.content}</p>
          <p>Post UserName: {post.username}</p>
          # 投稿編集ボタンを追加
          <button onClick={() => navigate(`/posts/${id}/edit`)}>編集</button>
          # 投稿削除ボタンを追加
          <button onClick={handleDelete}>削除</button>
        </div>
      ) : (
        <p>Post not found.</p>
      )}
    </div>
  );
};

※実際のアプリケーション開発では、ユーザーや投稿を削除する際は、「本当に削除しますか?」等の「確認画面」を設けるのが一般的です。

8-6. 各ページに、「Topページに戻る」リンクと「前のページに戻る」リンクを追加

Topページ(投稿一覧)以外のページに、「Topページに戻る」リンクと「前のページに戻る」リンクを追加し、ページ間のつながりを作ります。

※今回は、「前のページに戻る」リンクの遷移先を「Link to={`/posts/${id}`}」のように直接指定していますが、useHistoryを使用して、ブラウザの履歴から前のページを取得して遷移する方法もあります。

投稿作成ページ

【src/components/posts/PostCreate.jsx】

import { useState } from 'react';
# Linkコンポーネントをreact-router-domから呼び出す。
import { Link, useNavigate } from "react-router-dom";
import { postPost } from "../../infra/api";

export const PostCreate = () => {

・・・省略・・・

  return (
    <div>
      <h2>投稿作成</h2>
      # 「Topページに戻る」リンクを追加
      <div>
        <Link to={`/`}>Topページに戻る</Link>
      </div>
      # 「前のページに戻る」リンクを追加
      <div>
        <Link to={`/`}>前のページに戻る</Link>
      </div>
      <form onSubmit={handleSubmit}>

・・・省略・・・

      </form>
    </div>
  );
};

投稿詳細ページ

【src/components/posts/Post.jsx】

import { useEffect, useState } from "react";
# Linkコンポーネントをreact-router-domから呼び出す。
import { Link, useParams, useNavigate } from "react-router-dom";
import { getPost, deletePost } from "../../infra/api";

export const Post = () => {

・・・省略・・・

  return (
    <div>
      <h2>投稿詳細</h2>
      # 「Topページに戻る」リンクを追加
      <div>
        <Link to={`/`}>Topページに戻る</Link>
      </div>
      # 「前のページに戻る」リンクを追加
      <div>
        <Link to={`/`}>前のページに戻る</Link>
      </div>
      {loading ? (

・・・省略・・・

      )}
    </div>
  );
};

投稿編集ページ

【src/components/posts/PostEdit.jsx】

import { useEffect, useState } from "react";
# Linkコンポーネントをreact-router-domから呼び出す。
import { Link, useParams, useNavigate } from "react-router-dom";
import { getPost, updatePost } from "../../infra/api";

export const PostEdit = () => {

・・・省略・・・

  return (
    <div>
      <h2>投稿編集</h2>
      <div>
        <Link to={`/`}>Topページに戻る</Link>
      </div>
      <div>
        <Link to={`/posts/${id}`}>前のページに戻る</Link>
      </div>
      {loading ? (

・・・省略・・・

      )}
    </div>
  );
};

これでページ間のリンクが作成できました。

作成したアプリケーションをブラウザから確認してみましょう。

9. 作成したアプリケーションを確認する

$ docker-compose up -dコマンドを実行し、Dockerコンテナを起動します。

そして「localhost: 3001」にアクセスし、投稿一覧、投稿作成、投稿詳細、投稿編集ページそれぞれが表示されていることを確認します。

投稿一覧ページ(投稿なし)

投稿作成ページ

投稿してみます。

投稿一覧ページ(投稿あり)

投稿詳細ページ

投稿編集ページ

作成した投稿が表示されることが確認できました!

終わりに

現在のアプリケーションでは「認証機能を実装していないため、他のユーザーの投稿を作成、編集、削除ができてしまう」等の問題があります。

この記事を終えた後のステップアップ記事である「【手順解説】ReactとRails APIでタスク管理サービスを作る(準備中)」では、「認証機能の実装」について詳しく解説しています。