はじめに
「一覧画面にページャをつけたいんだけど、ライブラリ入れるのはめんどくさい・・・」
「ライブラリ入れるのはいいんだけど、見た目とかカスタマイズしたいんだよな・・・」
「かといってライブラリのカスタマイズはちょっとハードル高いし・・・」
ページャの実装でお悩みのそこのあなた。
この記事を読んで、ページャをさくっと作れるようになりましょう。
実物はこちら
リポジトリはこちら
概要
この記事を読むことで、以下のような画面が作れるようになります。
以下の仕様を想定して作成しています。
- 初期表示時は検索された状態
- 1ページあたりの表示件数は2件(データ作るのがめんどくさかったです、すみません)
- ページャの数字ボタンが押されたら、押された数字のページを表示する
- ページャの矢印ボタンは一つ前のページ、一つ後のページを表示する
- 検索ボタンが押されたら、表示ページをリセットして1ページ目を表示する
実装
先にコード全容です。
'use client'
import '../style.css'
import {Box, Button} from "@mui/material";
import {Input} from "@/components/input";
import {useTextInput} from "@/hooks/use-text-input";
import {tableUsers} from "@/data/data";
import {
FetchTableUsersWithPageRequestType,
FetchTableUsersWithPageResponseType,
PagerProps,
TableUser
} from "@/types/table";
import {CSSProperties, useEffect, useState} from "react";
const fetchUsers = async (param: FetchTableUsersWithPageRequestType): Promise<FetchTableUsersWithPageResponseType> => {
if (process.env.NEXT_PUBLIC_USE_MOCK_DATA === 'true') {
const filteredUsers = tableUsers.filter(user => user.name.indexOf(param.name ?? '') > -1)
const from = (param.page - 1) * param.pageSize
const to = param.page * param.pageSize
return {
users: filteredUsers.slice(from, to),
totalCount: tableUsers.length,
}
} else {
const response = await fetch('/api/table/users-with-page', {
method: 'POST',
body: JSON.stringify(param)
})
return await response.json()
}
}
const Pager = ({totalCount, page, pageSize, onPageChange}: PagerProps) => {
const totalPageCount = Math.ceil(totalCount / pageSize)
const pageButtonStyle: CSSProperties = {
border: '1px solid #000',
width: '30px',
textAlign: 'center'
}
return (
<div style={{display: 'flex', justifyContent: 'flex-end', marginTop: '1rem'}}>
{
// ページ数が2ページ以上のとき、「<」ボタンを表示する
totalPageCount > 1 &&
<div
style={{
...pageButtonStyle,
backgroundColor: page <= 1 ? '#dcdcdc' : 'transparent',
}}
onClick={() => {
if (page <= 1) return
onPageChange(page - 1)
}}
>
{'<'}
</div>
}
{
[...Array(totalPageCount)].map((_, index) => (
index + 1 === page
? (
<div key={index}
style={{
...pageButtonStyle,
marginLeft: '8px',
backgroundColor: '#f0f8ff'
}}
>
{index + 1}
</div>
)
: (
<div key={index}
style={{
...pageButtonStyle,
marginLeft: '8px'
}}
onClick={() => onPageChange(index + 1)}
>
{index + 1}
</div>
)
))
}
{
// ページ数が2ページ以上のとき、「>」ボタンを表示する
totalPageCount > 1 &&
<div
style={{
...pageButtonStyle,
marginLeft: '8px',
backgroundColor: page >= totalPageCount ? '#dcdcdc' : 'transparent',
}}
onClick={() => {
if (page >= totalPageCount) return
onPageChange(page + 1)
}}
>
{'>'}
</div>
}
</div>
)
}
export default function TableScratchWithPager() {
const [searchWord, setSearchWord] = useTextInput()
const [tableUserData, setTableUserData] = useState<Array<TableUser>>([])
const [totalCount, setTotalCount] = useState<number>(0)
const [currentPage, setCurrentPage] = useState<number>(1)
useEffect(() => {
(async () => {
const newUsers = await fetchUsers({name: null, page: 1, pageSize: 2})
setTableUserData(newUsers.users)
setTotalCount(newUsers.totalCount)
setCurrentPage(1)
})()
}, [])
const handleSearch = async (newPage: number) => {
const newUsers = await fetchUsers({name: searchWord.value, page: newPage, pageSize: 2})
setTableUserData(newUsers.users)
setTotalCount(newUsers.totalCount)
setCurrentPage(newPage)
}
return (
<main>
<Box m={2}>
<Box display="flex">
<Box>
<Input label="名前" errors={searchWord.errors} onChange={(e) => setSearchWord(e.target.value)}/>
</Box>
<Box ml={3} pt={3}>
<Button
type="button"
variant="contained"
disabled={searchWord.errors.length > 0}
onClick={() => handleSearch(1)}
>
検索
</Button>
</Box>
</Box>
</Box>
<Box m={2}>
<table>
<thead>
<tr>
<th>名前</th>
<th>所属</th>
<th>好きなもの</th>
</tr>
</thead>
<tbody>
{
tableUserData.map((user) => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.department}</td>
<td>{user.favoriteThings}</td>
</tr>
))
}
</tbody>
</table>
<Pager
totalCount={totalCount}
page={currentPage}
pageSize={2}
onPageChange={handleSearch}
/>
</Box>
</main>
)
}
各部分の解説
APIをページ指定できるよう改修
UIはもちろんですが、APIもページ指定ができるようにする必要があります。
今回は TypeScript のメモリ上で配列をあれこれしていますが、実際にはSQLの offset と limit でやると思います。
import {NextRequest, NextResponse} from "next/server"
import {tableUsers} from "@/data/data";
import {FetchTableUsersWithPageRequestType, TableUser} from "@/types/table";
export async function POST(request: NextRequest): Promise<NextResponse> {
const params: FetchTableUsersWithPageRequestType = await request.json()
const filteredUsers: TableUser[] = tableUsers.filter(user => user.name.indexOf(params.name ?? '') > -1)
const from = (params.page - 1) * params.pageSize
const to = params.page * params.pageSize
return NextResponse.json({
users: filteredUsers.slice(from, to),
totalCount: filteredUsers.length
})
}
リクエストとレスポンスも調整が必要です。
リクエストには、表示したいページ page
、1ページあたりの表示件数 pageSize
を追加します。
レスポンスには、検索でヒットした総件数 totalCount
を追加します。
export type FetchTableUsersWithPageRequestType = {
name: string | null,
page: number,
pageSize: number
}
export type FetchTableUsersWithPageResponseType = {
users: TableUser[],
totalCount: number
}
ページャコンポーネント
今回の肝となるページャコンポーネントです。
以下の引数が定義されていることを確認してください。
totalCount
: 検索でヒットした総件数page
: 表示するページpageSize
: 1ページあたりの表示件数onPageChange
: ページ変更時の処理
ポイントとなるのは、2行目で totalCount
と pageSize
から総ページ数 totalPageCount
を算出しているところです。
ここで算出した totalPageCount
を含めた4つの値(totalCount
page
pageSize
totalPageCount
)を使うことで、ページャのUIを実現することができます。
const Pager = ({totalCount, page, pageSize, onPageChange}: PagerProps) => {
const totalPageCount = Math.ceil(totalCount / pageSize)
const pageButtonStyle: CSSProperties = {
border: '1px solid #000',
width: '30px',
textAlign: 'center'
}
return (
<div style={{display: 'flex', justifyContent: 'flex-end', marginTop: '1rem'}}>
{
// ページ数が2ページ以上のとき、「<」ボタンを表示する
totalPageCount > 1 &&
<div
style={{
...pageButtonStyle,
backgroundColor: page <= 1 ? '#dcdcdc' : 'transparent',
}}
onClick={() => {
if (page <= 1) return
onPageChange(page - 1)
}}
>
{'<'}
</div>
}
{
[...Array(totalPageCount)].map((_, index) => (
index + 1 === page
? (
<div key={index}
style={{
...pageButtonStyle,
marginLeft: '8px',
backgroundColor: '#f0f8ff'
}}
>
{index + 1}
</div>
)
: (
<div key={index}
style={{
...pageButtonStyle,
marginLeft: '8px'
}}
onClick={() => onPageChange(index + 1)}
>
{index + 1}
</div>
)
))
}
{
// ページ数が2ページ以上のとき、「>」ボタンを表示する
totalPageCount > 1 &&
<div
style={{
...pageButtonStyle,
marginLeft: '8px',
backgroundColor: page >= totalPageCount ? '#dcdcdc' : 'transparent',
}}
onClick={() => {
if (page >= totalPageCount) return
onPageChange(page + 1)
}}
>
{'>'}
</div>
}
</div>
)
}
表示しているページによっては、スタイルや処理分岐をしている点に注意してください。
例えば、1ページ目を表示しているのに<
ボタンが押せてしまうと、「0ページ目を表示する」というよくわからないことになってしまいますよね。
あとは、2ページ目を表示しているときに2
ボタンが押せてしまうと、無駄な検索処理が発生してしまいます。
ページャコンポーネントを使う
ページャコンポーネントができたので、早速使ってみましょう。
コンポーネント化しているので、必要なパラメータを渡すのみです。
<table>
<thead>
<tr>
<th>名前</th>
<th>所属</th>
<th>好きなもの</th>
</tr>
</thead>
<tbody>
{
tableUserData.map((user) => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.department}</td>
<td>{user.favoriteThings}</td>
</tr>
))
}
</tbody>
</table>
<Pager
totalCount={totalCount}
page={currentPage}
pageSize={2}
onPageChange={handleSearch}
/>
</Box>
</main>
)
}
おわりに
いかがだったでしょうか?
今回は、3大めんどくさい UI の一つ、ページャを実装してみました。
より丁寧に作るのであれば、ページ数が多いとき 「1, 2, 3, … 20」というように省略表示したりなどが考えられますね。
あとは、先頭ページと最終ページを表示するボタンもあると便利かもしれません。
いずれの処理も、今回作成したコンポーネントをベースに作ることができると思います。
ぜひ、作ってみてくださいね。
ハコラトリは、Reactを習得したい駆け出しエンジニアを応援しています。
Reactレシピでは、これからもReactで色々なUIを作って紹介していきますので、「こんなUIを作ってみてほしい」「こんな場合はどうすれば?」といったアイディアを募集中です!
コメントお待ちしております。
コメント