RailsとNext.jsでシンプルなページネーションを実装する | Tomoyuki Kato's Blog

RailsとNext.jsでシンプルなページネーションを実装する

Engineering
この記事を書いた人

PharmaXというオンライン薬局のスタートアップで薬剤師・エンジニアとして働いています。Rails・React・TypeScriptなどを書きます。英語が得意でTOEIC900点・通訳案内士資格取得。主に薬剤師の働き方やプログラミング、英語学習について書きます。当サイトではアフィリエイトプログラムを利用して商品を紹介しています。
>> 詳しいプロフィール

Tomoyuki Katoをフォローする

 

この記事を読むと、以下のようなページネーションを実装できる。

実際の使用感とかはこのページなどをご確認ください。

 

全体の流れとしては、

  • Next.jsから何ページ目の情報がほしいのかのリクエストをする
  • リクエストをRailsが受け取る
  • Railsが総ページ数とXページ目の表示に必要なデータを返す。

というもの。

 

誰かの参考になれば嬉しい。

 

 

Next.jsでのページネーションの実装

 

今回紹介するNext.jsのページネーションでは以下のように実装した。

 

Paginationコンポーネントの実装

import React from "react";
import styled from "styled-components";
import colors from "../constans/colors";

const PaginationWrapper = styled.div`
  display: flex;
  justify-content: center;
  padding: 1em 0;
  list-style: none;
`;

const BaseButton = styled.a`
  margin: 0 0.5em;
  padding: 0.5em 1em;
  text-decoration: none;
  border-radius: 3px;
  transition: background-color 0.3s, color 0.3s;
`;

const PreviousButton = styled(BaseButton)`
  color: ${colors.text.gray.light};
  background-color: transparent;
  border: none;

  &:hover {
    color: ${colors.text.gray.medium};
    background-color: #f7fafc;
    cursor: pointer;
  }
`;

const NextButton = styled(BaseButton)`
  color: ${your-favorite-color};
  background-color: ${your-favorite-color};
  border: 1px solid ${your-favorite-color};

  &:hover {
    background-color: ${your-favorite-color};
    color: ${your-favorite-color};
    cursor: pointer;
  }
`;

type PaginationProps = {
  currentPage: number;
  totalPages: number;
  onPageChange: (selectedItem: { selected: number }) => void;
};

export const Pagination: React.FC = ({
  currentPage,
  totalPages,
  onPageChange,
}) => {
  const handlePrevious = () => onPageChange({ selected: currentPage - 2 });
  const handleNext = () => onPageChange({ selected: currentPage });

  return (
    
      {currentPage > 1 && (
        
          ← {currentPage - 1} ページへ
        
      )}
      {currentPage < totalPages && (
        次のページへ →
      )}
    
  );
};

 

usePaginationカスタムフックの実装

import { useRouter } from "next/router";
import { useEffect, useState } from "react";

interface PaginationHook {
  currentPage: number;
  handlePageClick: (data: { selected: number }) => void;
}

export const usePagination = (initialPage: number = 1): PaginationHook => {
  const router = useRouter();
  const [currentPage, setCurrentPage] = useState(initialPage);

  useEffect(() => {
    setCurrentPage(Number(router.query.page) || 1);
  }, [router.query.page]);

  const handlePageClick = (data: { selected: number }): void => {
    const nextPage = data.selected + 1;

    if (nextPage === 1 && currentPage > 1) {
      router.push({
        pathname: router.pathname,
        query: { ...router.query, page: nextPage },
      });
    } else if (nextPage === 1) {
      const { page, ...rest } = router.query;
      router.push({
        pathname: router.pathname,
        query: { ...rest },
      });
    } else {
      router.push({
        pathname: router.pathname,
        query: { ...router.query, page: nextPage },
      });
    }
  };

  return { currentPage, handlePageClick };
};

 

useArticlesカスタムフックの実装

import { useState, useEffect } from "react";
import {
  ArticlesResponse,
  getArticles,
} from "../api/get-articles";

interface UseArticlesHook {
  response: ArticlesResponse | null;
  loading: boolean;
  fetchArticles: (page: number) => void;
}

export const useArticles = (
  initialPage: number = 1
): UseArticlesHook => {
  const [response, setResponse] = useState(
    null
  );
  const [loading, setLoading] = useState(true);

  const fetchArticles = async (page: number) => {
    setLoading(true);
    try {
      const res = await getArticles(page);
      setResponse(res);
    } catch (error) {
      console.error("Failed to fetch articles", error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchArticles(initialPage);
  }, [initialPage]);

  return { response, loading, fetchArticles };
};

 

APIの実装

import axiosApi from "../../../libs/axios/client";

type NewsArticle = {
  id: number;
  categoryName: string;
  sourceUrl: string;
  title: string;
  url: string;
  publishedAt: string;
  permaLink: string;
  thumbnailUrl: string;
};

export type ArticlesResponse = {
  articles: NewsArticle[];
  totalPages: number;
};

export const getArticles = async (
  page: number = 1
): Promise =>
  axiosApi
    .get("news/get_articles", {
      params: {
        page: page,
      },
    })
    .then((res) => {
      return res.data;
    });

 

client.tsの実装

今回はaxiosを使用。

import applyCaseMiddleware from "axios-case-converter";
import axios from "axios";

const options = {
  ignoreHeaders: true,
};

const axiosApi = applyCaseMiddleware(
  axios.create({
    baseURL: process.env.NEXT_PUBLIC_API_URL,
  }),
  options
);

export default axiosApi;

 

以上でフロントの実装は終わり。

 

Railsでのページネーション実装

 

次にRailsでの実装。

 

controllerの実装

class GetDrugStoreArticlesController < ApplicationController
   def index
    request = ArticlesGetRequest.new(params)
    per_page = 10

    total_pages = Article.find_article_pages(per_page)
    articles = Article.find_articles(
      page: request.page, per_page: per_page
     )

    response = ArticlesGetResponse.new(
       articles: articles,
       total_pages: total_pages
     )

   render json: response.to_hash, status: :ok
 end
end

 

Requestクラスの実装

 class ArticlesGetRequest
   attr_reader :page

   def initialize(params:)
     @page = params[:page].to_i
   end
 end

 

ArticleModelの実装

Class Article
 class << self
  def find_article_pages(per_page:)
    articles_num = Article.count
    (articles_num.to_f / per_page).ceil
  end

  def find_articles(per_page:, offset:)
    offset = (page - 1) * per_page
    articles = Article.offset(offset).limit(per_page)
  end
 end
end

 

Responseクラスの実装

class ArticlesGetResponse
 def initialize(articles:, total_pages:)
   @articles = articles
   @total_pages = total_pages
 end

 def to_hash
   {
     articles: articles_list,
     total_pages: @total_pages
    }
 end

 private

 def articles_list
   @articles.list.map do |article|
     {
       id: article.id,
       title: article.title,
       publishedAt: article_entity.published_at,
       url: article_entity.url,
       thumbnail_url: article.thumbnail_url
      }
    end
  end
end

 

以上で実装終わり。

タイトルとURLをコピーしました