この記事を読むと、以下のようなページネーションを実装できる。
実際の使用感とかはこのページなどをご確認ください。
全体の流れとしては、
- 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
以上で実装終わり。