はじめに
「ライブラリのページャを使ってるんだけど、見た目をカスタマイズしたいな・・・」
「でもカスタマイズの仕方わからないな・・・」
「ページャを作るロジックだけ使えればいいんだけどな・・・」
そう思ったことはありませんか?
それ、MUI の usePagination を使えば実現します。
この記事では、自作のページャの表示ロジックを MUI が提供している usePagination に差し替えた方法をシェアします。
ページャをきちんと作ろうと思うと、実はけっこう大変です。
ページ数が多いときはレイアウトが崩れないよう、溢れたページを省略表示しないといけません。
省略表示の方法も、現在ページによって微妙に異なります。
勉強がてら自分で作ってみるのもいいですが、「ページャはさっさと片付けてもっと別のことに集中したい」という方はライブラリを使うべきでしょう。
使えるものは使ったほうがよいです。
ただし、ライブラリを使うと気にしなくてはならないのが「見た目の統一」です。
独自のデザインの画面の中に、ポツンとライブラリのページャが使われていたら、きっとそこだけ異質に見えてしまうでしょう。
見た目をちゃちゃっとカスタマイズできればよいですが、難しい場合も考えられます。
そんなときは、この記事で紹介する「表示ロジックだけライブラリを使う方法」を試してみてください。
面倒な表示ロジックだけはライブラリを使い、見た目は自分好みに作る。
そんないいとこ取りの方法をご紹介します。
実物はこちら
リポジトリはこちら
概要
この記事を読むことで、以下のような画面が作れるようになります。
実装
先にコード全容です。
'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,
PageButtonProps,
RichPagerProps,
TableUser
} from "@/types/table";
import {ChangeEvent, CSSProperties, useEffect, useState} from "react";
import usePagination, {UsePaginationItem} from "@mui/material/usePagination";
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 PageButton = ({children, onClick, disabled, selected}: PageButtonProps) => {
const pageButtonStyle: CSSProperties = {
border: '1px solid #000',
minWidth: '35px',
textAlign: 'center',
padding: "5px 0",
marginLeft: '8px',
}
return (
<Button
style={{
...pageButtonStyle,
backgroundColor: selected ? '#f0f8ff' : 'transparent'
}}
onClick={onClick}
disabled={disabled}
>
{children}
</Button>
)
}
const Pager = ({totalCount, page, pageSize, onPageChange}: RichPagerProps) => {
const totalPageCount = Math.ceil(totalCount / pageSize)
// 表示中の件数表示 from - to
const from = (page - 1) * pageSize + 1
const to = page < totalPageCount ? page * pageSize : totalCount
const {items} = usePagination({
count: totalPageCount,
page: page,
onChange: onPageChange
})
const getButtonLabel = (item: UsePaginationItem) => {
switch (item.type) {
case 'first':
return '<<'
case 'previous':
return '<'
case 'start-ellipsis':
case 'end-ellipsis':
return '...'
case 'page':
return item.page
case 'next':
return '>'
case 'last':
return '>>'
}
}
return (
<div style={{display: 'flex', justifyContent: 'flex-end', marginTop: '1rem'}} className="page-buttons">
{
totalCount > 0 &&
<div style={{marginRight: '16px', padding: '5px 0'}}>
{from} - {to} of {totalCount}
</div>
}
{
items.map((item, index) => (
<PageButton
key={index}
onClick={item.onClick}
disabled={item.disabled}
selected={item.selected}
>
{getButtonLabel(item)}
</PageButton>
))
}
</div>
)
}
export default function TableScratchWithPagerRich() {
const [searchWord, setSearchWord] = useTextInput()
const [tableUserData, setTableUserData] = useState<Array<TableUser>>([])
const [totalCount, setTotalCount] = useState<number>(0)
const [currentPage, setCurrentPage] = useState<number>(1)
const [pageSize, setPageSize] = useState<number>(5)
useEffect(() => {
(async () => {
const newUsers = await fetchUsers({name: null, page: 1, pageSize})
setTableUserData(newUsers.users)
setTotalCount(newUsers.totalCount)
setCurrentPage(1)
})()
}, [])
const handleSearch = async (event: ChangeEvent<unknown>, newPage: number) => {
const newUsers = await fetchUsers({name: searchWord.value, page: newPage, pageSize})
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={(e) => handleSearch(e, 1)}
>
検索
</Button>
</Box>
</Box>
</Box>
<Box m={2}>
<Pager
totalCount={totalCount}
page={currentPage}
pageSize={pageSize}
onPageChange={handleSearch}
/>
<table style={{marginTop: '1rem'}}>
<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={pageSize}
onPageChange={handleSearch}
/>
</Box>
</main>
)
}
各部分の解説
ページャの表示ロジックを usePagination に差し替える
差し替え前の表示ロジックを以下に記載します。
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>
)
}
条件分岐が何個もあり、省略表示の処理も入れられていません。
次に、表示ロジックを usePagination に差し替えたコードを以下に記載します。
const Pager = ({totalCount, page, pageSize, onPageChange}: RichPagerProps) => {
const totalPageCount = Math.ceil(totalCount / pageSize)
// 表示中の件数表示 from - to
const from = (page - 1) * pageSize + 1
const to = page < totalPageCount ? page * pageSize : totalCount
const {items} = usePagination({
count: totalPageCount,
page: page,
onChange: onPageChange
})
const getButtonLabel = (item: UsePaginationItem) => {
switch (item.type) {
case 'first':
return '<<'
case 'previous':
return '<'
case 'start-ellipsis':
case 'end-ellipsis':
return '...'
case 'page':
return item.page
case 'next':
return '>'
case 'last':
return '>>'
}
}
return (
<div style={{display: 'flex', justifyContent: 'flex-end', marginTop: '1rem'}} className="page-buttons">
{
totalCount > 0 &&
<div style={{marginRight: '16px', padding: '5px 0'}}>
{from} - {to} of {totalCount}
</div>
}
{
items.map((item, index) => (
<PageButton
key={index}
onClick={item.onClick}
disabled={item.disabled}
selected={item.selected}
>
{getButtonLabel(item)}
</PageButton>
))
}
</div>
)
}
ハイライトした箇所が usePagination を使用しているところです。
usePagination を呼ぶことで、ページャを表示するための items
が返されます。items
には、省略表示はもちろん、現在ページのフラグ、次に進む、前に戻るなど、ページャに必要な情報が含まれています。
なので、あとは items
を使ってページャを表示すればOKです。
とても簡単ですね!
条件分岐がなくなり、コードがスッキリしていることを確認してください。
ページャのボタンの見た目をカスタマイズする
ページャの表示ロジックがクリアできたので、見た目をカスタマイズしていきましょう。
ページャの各ボタンは PageButton
コンポーネントで表示するようにしています。
const PageButton = ({children, onClick, disabled, selected}: PageButtonProps) => {
const pageButtonStyle: CSSProperties = {
border: '1px solid #000',
minWidth: '35px',
textAlign: 'center',
padding: "5px 0",
marginLeft: '8px',
}
return (
<Button
style={{
...pageButtonStyle,
backgroundColor: selected ? '#f0f8ff' : 'transparent'
}}
onClick={onClick}
disabled={disabled}
>
{children}
</Button>
)
}
以下の設定をしていることを確認してください。
- スタイル設定:
pageButtonStyle
でボタンの基本のスタイルを設定しています。 - 現在ページのスタイル設定:
usePagination
から返されるitems
には、現在ページかどうかを表すselected
というプロパティが含まれています。これがtrue
のとき、ボタンの色を変えてハイライトするようにしています。 - ボタン非活性のスタイル設定:
usePagination
から返されるitems
には、ボタンが使用可能かどうかを表すdisabled
というプロパティが含まれています。例えば、現在ページが 1 のとき、前に戻るボタンは使えないのでdisabled=true
となります。
おわりに
いかがだったでしょうか?
今回は、自作のページャの表示ロジックを MUI が提供している usePagination に差し替える方法を紹介しました。
usePagination は React のカスタムフックという機能で実装されています。
カスタムフックは、React のコードのうちのロジック部分を別関数に分離するためのものです。
ページャの表示ロジックを usePagination に差し替えたことで、差し替え前にあった条件分岐がごっそりなくなったことがおわかりいただけたかと思います。
ページャに限らず何かアプリを作ろうと思ったとき、カスタムフックが提供されていれば同じように活用できるはずです。
「表示ロジックは実装したくないけど、見た目にはこだわりたい」というときは、ライブラリでカスタムフックが提供されていないか、探してみてはいかがでしょうか?
ご自身でアプリを作るときも、カスタムフックで表示ロジックを分離することはいい選択です。
ぜひ試してみてくださいね。
ハコラトリは、Reactを習得したい駆け出しエンジニアを応援しています。
Reactレシピでは、これからもReactで色々なUIを作って紹介していきますので、「こんなUIを作ってみてほしい」「こんな場合はどうすれば?」といったアイディアを募集中です!
コメントお待ちしております。
コメント