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