#概要
**前回作成したアプリにvte.cx**でページネーションの機能を実装しました。
**vte.cxについて詳しく知りたい方はvte.cxのドキュメント**をご覧ください。
今回実装したページネーションの仕様は以下になります。
- 初期表示は1ページ目
- 総件数を取得する
- 「次へ」ボタンを押すと、+1ページ目に移動する
- 「前へ」ボタンを押すと、-1ページ目に移動する
- 「最初」ボタンを押すと、1ページ目に移動する
- 「最後」ボタンを押すと、最後のページに移動する
- 現在のページを表示する
- ページの最終番号以降のリクエストが来た時にindexを追加で貼り直す
#実装
今回ページネーションを実装したファイルは、前回作った一覧画面(ListProf)と、今回新しく作成したページネーション(Pagination)ファイルになります。
##一覧画面(ListProf)。
import * as React from 'react'
import { useState, useRef } from 'react'
import axios from 'axios'
import { withRouter } from 'react-router-dom'
import Pagination from './Pagination'
import SearchList from './SearchList'
//一覧画面
const ListProf = (props:any) => {
const [deletedPage, setDeletedPage] = useState(1)
const [feed, setFeed] = useState<VtecxApp.Entry[]>([])
const [firstIndexPage, setFirstIndexPage] = useState(1)
const [lastIndexPage, setLastIndexPage] = useState(50)
const feedLength = useRef(0)
const lastPage = useRef(0)
//データの総件数取得関数
const getFeedLength = async() => {
try {
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
const res = await axios.get('/d/foo?f&c&l=*')
feedLength.current = (Number(res.data.feed.title))
lastPage.current = (Math.ceil(res.data.feed.title / 5))
} catch {
alert('総件数の取得に失敗しました。')
}
}
//pageindex作成リクエスト関数
const putIndex = async(currentPage:number) => {
try {
let firstIndex = firstIndexPage
let lastIndex = lastIndexPage
if(currentPage > firstIndexPage && currentPage > lastIndexPage){
firstIndex = firstIndex + 50
lastIndex = lastIndex + 50
}
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
await axios.get('/d/foo?f&_pagination='+firstIndex+','+lastIndex+'&l=1')
setFirstIndexPage(firstIndex)
setLastIndexPage(lastIndex)
} catch {
alert('indexが貼れませんでした。')
}
}
//指定ページの一覧取得関数
const getPage = async(currentPage:number, retry_count:number) => {
try {
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
const res = await axios.get('/d/foo?f&n='+ currentPage + '&l=5')
setFeed(res.data)
} catch(e) {
const retry = () => {
retry_count++
const retryIndex = () => {
getPage(currentPage, retry_count)
}
if(retry_count > 9) {
alert('ページが取得できませんでした')
return false
}else{
setTimeout(() => retryIndex(),1000)
}
}
//indexが貼れていなかった場合10回までindexリクエストをリトライする
if(e.response.data.feed.title === "Please make a pagination index in advance.") {
retry()
//ページの最終番号以降のリクエストが来た時にindexを追加で貼り直す
} else if(e.response.data.feed.title === "Please set a positive number for Page number.") {
putIndex(currentPage)
retry()
}
}
}
//削除関数
const deleteEntry = async (entry:VtecxApp.Entry, index:number) => {
try {
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
if(entry && entry.link) {
const key = entry.link[0].___href
await axios.delete('/d'+ key)
getFeedLength()
putIndex(deletedPage)
if(index === 0 && deletedPage !== 1){
getPage(deletedPage -1, 0)
} else {
getPage(deletedPage, 0)
}
}
alert('削除しました')
} catch (e) {
alert('削除できませんでした')
}
}
return (
<div>
{ feed.length > 0 ?
<div>
<SearchList setFeed={setFeed}
feedLength={feedLength.current}
lastPage={lastPage.current} />
<table>
<tr>
<th>名前</th>
<th>メール</th>
<th>職業</th>
<th>住所</th>
<th>身長</th>
<th>誕生日</th>
<th>性別</th>
<th>チェック</th>
<th>セレクト</th>
<th>メモ</th>
</tr>
{feed.map((entry, index) => (
<tr>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.name}, data: entry, title: 'name', type: 'text'})}>
{entry.user!.name}</a></td>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.email},data: entry, title: 'email', type: 'text'})}>
{entry.user!.email}</a></td>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.job},data: entry, title:'job', type: 'text'})}>
{entry.user!.job}</a></td>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.address},data: entry, title: 'address', type: 'text'})}>
{entry.user!.address}</a></td>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.height},data: entry, title: 'height', type: 'text'})}>
{entry.user!.height}</a></td>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.birthday},data: entry, title: 'birthday'})}>
{entry.user!.birthday}</a></td>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.gender},data: entry, title: 'gender'})}>
{entry.user!.gender}</a></td>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.check},data: entry, title: 'check'})}>
{entry.user!.check}</a></td>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.select},data: entry, title: 'select'})}>
{entry.user!.select}</a></td>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.memo},data: entry, title: 'memo'})}>
{entry.user!.memo}</a></td>
<td><button onClick={() => deleteEntry(entry, index)}>削除</button></td>
</tr>
))}
</table>
</div>
:
<p>登録されていません</p>
}
<Pagination
setDeletedPage={setDeletedPage} getFeedLength={getFeedLength}
getPage={(e:number) => getPage(e, 0)} putIndex={(e:number) => putIndex(e)}
feedLength={feedLength.current} lastPage={lastPage.current}
lastIndexPage={lastIndexPage}
/>
<button onClick={() => props.history.push('/RegisterProf')}>新規登録</button>
</div>
)
}
export default withRouter(ListProf)
前回あったgetFeed関数がなくなってgetFeedLength, PutIndex, getPage関数が増えています。
ページネーションを実装するには、以下の3つのリクエストを実行する必要があり,3つのリクエストをgetFeedLength, PutIndex, getPage関数で実行しています。
- 一覧の総件数取得リクエスト(getFeedLength)
- カーソル(pageindex)作成リクエスト(putIndex)
- 指定ページの一覧取得リクエスト(getPage)
まずは、**一覧の総件数取得リクエスト(getFeedLength)**から見ていきましょう。
###一覧の総件数取得リクエスト(getFeedLength)
const feedLength = useRef(0)
const lastPage = useRef(0)
//データの総件数取得関数
const getFeedLength = async() => {
try {
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
const res = await axios.get('/d/foo?f&c&l=*')
feedLength.current = (Number(res.data.feed.title))
lastPage.current = (Math.ceil(res.data.feed.title / 5))
} catch {
alert('総件数の取得に失敗しました。')
}
}
この関数で実行される大まかな流れは、以下のようになります。
- 変数
res
で総件数取得リクエストを実行 -
res
で受け取った総件数をfeedLength
に格納 -
res
で受け取った総件数を1ページあたりに表示する件数(5件)で割り、最終ページを計算してlastPage
に格納 - エラーが出た場合はアラートを出す
tryで最初に実行される、HTTPヘッダにX-Requested-With: XMLHttpRequest
をつけているのはセキュリティ(CSRF対策)のためですが、ここではおまじないだと思って必ずつけるということだけ覚えてください。
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
const res = await axios.get('/d/foo?f&c&l=*')
総件数はc&l=*
で取得しています。
cで配下のエントリ件数を取得し、lで取得可能なエントリの最大件数を指定しています。l=*
とすると取得総件数を無制限にできます。
つまり、変数res
で/d/foo
配下のエントリの総件数(無制限)を取得する。ということになります。
f:配下のエントリ一覧を取得
l:取得可能なエントリの最大件数(*ですべて)
c:配下のエントリ件数を取得
実際に取得したエントリをconsole.log(res)
で出力してみると、下の画像のようになると思います。
res.data.feed.title
の中に入っている"64"
が総件数になります。
よく見ると、64がダブルクォーテーションで囲まれているのが分かると思います。
これをNumber
で数字に変換して、feedLength
に総件数として格納しています。
lastPage
も同じ感じで格納するのですが、計算した値が割り切れなかった際に、繰り上げをして最終ページを算出したいので、Math.ceil
を使い総件数(今回は64件)を1ページあたりに表示したい件数(5件)で割って、lastPage
に最終ページとして格納しています。
feedLength.current = (Number(res.data.feed.title))
lastPage.current = (Math.ceil(res.data.feed.title / 5))
次に**カーソル(pageindex)作成リクエスト(putIndex)**を見ていきましょう。 ###カーソル(pageindex)作成リクエスト(putIndex)
const putIndex = async(currentPage:number) => {
try {
let firstIndex = firstIndexPage
let lastIndex = lastIndexPage
if(currentPage > firstIndexPage && currentPage > lastIndexPage){
firstIndex = firstIndex + 50
lastIndex = lastIndex + 50
}
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
await axios.get('/d/foo?f&_pagination='+firstIndex+','+lastIndex+'&l=5')
setFirstIndexPage(firstIndex)
setLastIndexPage(lastIndex)
} catch {
alert('indexが貼れませんでした。')
}
}
この関数で実行される大まかな流れは、以下のようになります。
- 変数
firstIndex
とlastIndex
を定義し、カーソル作成リクエストを送る際の開始と最終ページ数を指定する - 現在のページ数(currentPage)が現在貼ってあるpageindexよりも値が大きかった場合
firstIndex
と
lastIndex
をプラス50する - カーソル作成リクエストを実行する
- 「3.」を実行した際の最初と最後のページ数を記憶しておくために、useStateを使い
firstIndexPage
にfirstIndex
、lastIndexPage
にlastIndex
を格納する - エラーが出た場合はアラートを出す
まずカーソル作成リクエストを実行しているところから説明します。
await axios.get('/d/foo?f&_pagination='+firstIndex+','+lastIndex+'&l=5')
カーソル作成リクエストを実行するには、上記のように
/d/foo?f
の後に_pagination={開始ページ},{最終ページ}&l={件数}
を挿入します。
大量のカーソル一覧(pageindex)を張るのは時間がかかるため、最初のリクエストは最終ページ数を50にとどめておきます。
let firstIndex = firstIndexPage
let lastIndex = lastIndexPage
if(currentPage > firstIndexPage && currentPage > lastIndexPage){
firstIndex = firstIndex + 50
lastIndex = lastIndex + 50
}
開始ページをfirstIndex
、最終ページをlastIndex
にしているのは、このputIndex関数を実行した際に現在のページ数(currentPage)が最終ページ(lastIndex)を超えていた場合に上記のように追加で50ページ分のカーソルを作成するために値を記憶しておく必要があるからです。
setFirstIndexPage(firstIndex)
setLastIndexPage(lastIndex)
そのため、カーソル作成リクエストを実行した後に、setFirstIndexPage(firstIndex)
とsetLastIndexPage(lastIndex)
を実行して開始ページと最終ページを記憶しておきます。
最後に**指定ページの一覧取得リクエスト(getPage)**を見ていきましょう。
###指定ページの一覧取得リクエスト(getPage)
const getPage = async(currentPage:number, retry_count:number) => {
try {
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
const res = await axios.get('/d/foo?f&n='+ currentPage + '&l=5')
setFeed(res.data)
} catch(e) {
const retry = () => {
retry_count++
const retryIndex = () => {
getPage(currentPage, retry_count)
}
if(retry_count > 9) {
alert('ページが取得できませんでした')
return false
}else{
setTimeout(() => retryIndex(),1000)
}
}
//indexが貼れていなかった場合getPageをリトライする
if(e.response.data.feed.title === "Please make a pagination index in advance.") {
retry()
//ページの最終番号以降のリクエストが来た時にpageindexを追加で貼り直す
} else if(e.response.data.feed.title === "Please set a positive number for Page number.") {
putIndex(currentPage)
retry()
}
}
}
この関数で実行される大まかな流れは、以下のようになります。
- 指定ページの一覧取得リクエストを実行する
- 取得したデータを
setFeed
を使いfeed
に格納する - エラーが出た場合は10回までリトライする
まずは指定ページの一覧取得リクエストを実行するところから説明していきます。
const res = await axios.get('/d/foo?f&n='+ currentPage + '&l=5')
setFeed(res.data)
指定ページの一覧取得リクエストを実行するには、
上記のように/d/foo?f
の後にn={表示したいページ数}&l={表示したい件数}
を挿入します。
表示したいページ数は、ページ数を切り替えるボタンを押下した際にページ数を切り替えることができるようにcurrentPage
を指定します。表示したい件数は今回は5件とします。
console.log(res)
で出力してみると下の画像のようになると思います。
表示したい一覧は、res.data
の中に配列で入っています。
この配列を画面に表示させるために、setFeed
でfeed
の中に格納します。
次にエラーが出た際にリトライする処理について説明します。
指定ページの一覧を取得するには、あらかじめカーソル一覧(pageindex)を作成しておく必要があります。検索時、指定したページ数のカーソル一覧がセッションに存在しない場合は400エラーとなり、下の画像のように「Please make a pagination index in advance. 」のメッセージが返ります。また、検索した結果、最終ページ数に満たない場合は「Please set a positive number for Page number.」のメッセージが返ります。
特に「Please make a pagination index in advance. 」のエラーは度々起こります。putIndex
リクエストでカーソル作成リクエストを行っていますが、これは非同期で行っているため、カーソル(pageindex)が作成完了する前にレスポンスが返却されています。よって、カーソルが作成される前にページ取得リクエストを実行すると上記エラーが発生します。 これを解決するためには以下のようにif文を使いリトライ関数を実行します。
「Please set a positive number for Page number.」とエラーが出ている場合は、putIndexも一緒に実行することで、追加でカーソルを作成します。
//indexが貼れていなかった場合10回までindexリクエストをリトライする
if(e.response.data.feed.title === "Please make a pagination index in advance.") {
retry()
//ページの最終番号以降のリクエストが来た時にindexを追加で貼り直す
} else if(e.response.data.feed.title === "Please set a positive number for Page number.") {
putIndex(currentPage)
retry()
}
retry
関数が実行されると、getPage
の引数に指定してあるretry_count
をプラス1してsetTimeout
で1秒後にretryIndex
を実行し,リトライします。retry_count
でリトライした回数を数えて10回実行してもエラーが解決しなかったらリトライを止めてアラートを出します。
const retry = () => {
retry_count++
const retryIndex = () => {
getPage(currentPage, retry_count)
}
if(retry_count > 9) {
alert('ページが取得できませんでした')
return false
}else{
setTimeout(() => retryIndex(),1000)
}
}
以上の3つの関数を実行することで、ページネーションの機能が実装されます。
実際に関数を実行する処理はページネーションファイルの方になります。
では、ページネーションファイルの方を見ていきましょう。
###ページネーションファイル
import * as React from 'react'
import { useState, useEffect, useMemo} from 'react'
//ページネーションコンポーネント
const Pagination = (_props:any) => {
const [currentPage, setCurrentPage] = useState(1)
const memoPutIndex = useMemo(() => {
_props.putIndex(currentPage)
},[currentPage > _props.lastIndexPage])
const memoGetFeedLength = useMemo(() => {
_props.getFeedLength()
},[])
//画面表示処理
useEffect(() => {
memoGetFeedLength
memoPutIndex
_props.getPage(currentPage, 0)
_props.setDeletedPage(currentPage)
},[currentPage])
return (
<div>
<b>総件数:{_props.feedLength}</b><br />
<b>現在のページ:{currentPage}</b>
<ul>
<li><a href="javascript:void(0)"
onClick={() => currentPage === 1 ? null: setCurrentPage(1) }>最初</a></li>
<li><a href="javascript:void(0)"
onClick={() => currentPage === 1 ? null: setCurrentPage(currentPage - 1)}>前へ</a></li>
<li><a href="javascript:void(0)"
onClick={() => currentPage === _props.lastPage ? null: setCurrentPage(currentPage + 1)} >次へ</a></li>
<li><a href="javascript:void(0)"
onClick={() => currentPage === _props.lastPage ? null: setCurrentPage(_props.lastPage)}>最後</a></li>
</ul>
</div>
)
}
export default Pagination
一覧画面(ListProf)で定義したページネーションを実装するための関数をuseEffect
で実行しています。
ページ数が変わるたびに関数を実行するために、第2引数にcurrentPage
を指定します。
const memoPutIndex = useMemo(() => {
_props.putIndex(currentPage)
},[currentPage > _props.lastIndexPage])
const memoGetFeedLength = useMemo(() => {
_props.getFeedLength()
},[])
//画面表示処理
useEffect(() => {
memoGetFeedLength
memoPutIndex
_props.getPage(currentPage, 0)
_props.setDeletedPage(currentPage)
},[currentPage])
総件数の取得(getFeedLength)とカーソル作成(putIndex)はページ数が変わるたびに実行する必要はないため、useMemo
でメモ化しておきます。
ただし、putIndex
は現在のページ数が最終ページを超えていた場合だけ再実行できるように第2引数にcurrentPage > _props.lastIndexPage
を指定します。
return (
<div>
<b>総件数:{_props.feedLength}</b><br />
<b>現在のページ:{currentPage}</b>
<ul>
<li><a href="javascript:void(0)"
onClick={() => currentPage === 1 ? null: setCurrentPage(1) }>最初</a></li>
<li><a href="javascript:void(0)"
onClick={() => currentPage === 1 ? null: setCurrentPage(currentPage - 1)}>前へ</a></li>
<li><a href="javascript:void(0)"
onClick={() => currentPage === _props.lastPage ? null: setCurrentPage(currentPage + 1)} >次へ</a></li>
<li><a href="javascript:void(0)"
onClick={() => currentPage === _props.lastPage ? null: setCurrentPage(_props.lastPage)}>最後</a></li>
</ul>
</div>
)
あとはページを切り替えるためのボタンを作るだけです。
無限にページ数が増えたり減ったりしないようにif文で判定してnullを返すことでページ数が切り替わらないようにします。
#終わりに
今回はページネーションを実装する際の3つのリクエストがどのリクエストがなにをやっているのか理解できずに苦労しました。
また、useRefやuseMemoなどの今まで使っていなかったHooksを使ってみたりして、React HooksとVte.cxの理解が深まったと思います。
以上。