Firestoreの簡単な一覧、作成、更新、削除の処理はできたのですが一覧のページネーションがない状況なので ページネーションを追加しました。
この記事はReact Firebase入門シリーズです
1-1・ Firebase初期設定とFirebaseAuthのSignUp
1-2・ 「react-hook-form」を入れてみた
1-3・ AuthのSignUpの通信エラー対応
2 ・ FirebaseAuthのログイン処理
3 ・ FirebaseAuthのログイン認証とログアウト処理
4 ・ FirebaseAuthのパスワード初期化処理
5 ・ FirebaseAuthのメールアドレスの有効化
6 ・ FirebaseAuthのメールアドレスとパスワード変更
7 ・ FirebaseAuthの表示名変更
8-1・ FirebaseAuthのアバター変更:基礎
8-2・ FirebaseAuthのアバター変更:画像切り抜き
9-1・FirestoreのCRUD
9-2・Firestoreのデータ取得補足
9-3・Firestoreのページネーション処理 今ここ
ページネーションとは
次のページに遷移したり前のページしたりする事をページネーションといい、大きくわけて2つのやり方があります
- オフセットベース(Offset-based pagination)
- カーソルベース(Cursor-based pagination)
簡単なオフセットベースのページネーションを作りたかったがoffsetがなかった・・・
公式には書いてあったけど、console.logで確認したがなかった。
(´;ω;`)
しかもページネーションで一番大切な現在のページやレコード数も取れない事が判明。
本来はこのページネーションを作りたいのですが今回は次前のページリンクで逃げました
ページネーションの修正内容
前回のソースから改変した部分を記載したいと思います。
プログラム
index.js(一覧処理)しか直していません。 全部をみる人は少ないと思いますが念のための記載します
全文(長文なの注意してください)
import React, { useState, useEffect,useRef } from "react"; import { db } from "../../firebase" import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; import { Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, + Fab, } from '@material-ui/core'; import { Link } from "react-router-dom" import moment from 'moment'; + import { + NavigateBefore, + NavigateNext + } from "@material-ui/icons"; const useStyles = makeStyles((theme: Theme) => createStyles({ container: { padding: 16, margin: 'auto', maxWidth: 480 }, table: { margin: 'auto', minWidth: 600, maxWidth: 960 }, + largeIcon: { + width: 60, + height: 60, + }, }) ); + const LIMIT_COUNT = 10 export const Index = () => { // 入力用DOMノードへの参照を保持する const inputRef = useRef(); const classes = useStyles();//Material-ui //const history = useHistory() const [list, setList] = useState([]) + const [firstRecord, setFirstRecord] = useState() + const [lastRecord, setLastRecord] = useState() + const [currentfirstRecord, setCurrentFirstRecord] = useState() + const [currentlastRecord, setCurrentLastRecord] = useState() + const [hasPreviousPage, sethasPreviousPage] = useState() + const [hasNextPage, sethasNextPage] = useState() + useEffect(() => { + if (lastRecord && list) { + if (list.length > 0 ){ + sethasNextPage(true) + list.forEach((item) => { + //console.log("useEffect:list.item", item.docId) + if ( item.docId === lastRecord.docId){ + console.log("useEffect:lastRecord _ari", lastRecord) + sethasNextPage(false) + } + }) + //最後のレコードを取得する + setCurrentLastRecord(list.slice(-1)[0]); + } + } + if (firstRecord && list) { + if (list.length > 0 ){ + //console.log("useEffect:list",list) + //console.log("useEffect:firstRecord",firstRecord) + sethasPreviousPage(true) + list.forEach((item) => { + //console.log("useEffect:list.item", item.docId) + if ( item.docId === firstRecord.docId){ + console.log("useEffect:firstRecord _ari", firstRecord) + sethasPreviousPage(false) + } + }) + } + //最初のレコードを取得する + setCurrentFirstRecord(list[0]); + } + },[firstRecord,lastRecord,list]); useEffect(() => { async function fetchData() { // featchDataという関数を定義し、それにasyncをつける //最初のページを取得する const colRef = db.collection("members") .orderBy('createdAt', 'desc') + .limit(10); - .limit(LIMIT_COUNT); console.log(colRef) const snapshots = await colRef.get(); var docs = snapshots.docs.map(function (doc) { return doc.data(); }); setList(docs) } + async function initialData() { // featchDataという関数を定義し、それにasyncをつける + //最後のレコードをセットする + await db.collection("members").orderBy('createdAt', 'desc').limitToLast(1) + .get() + .then(function(querySnapshot) { + querySnapshot.forEach(function(doc) { + setLastRecord(doc.data()) + }); + }) + .catch(function(error) { + console.log("Error getting documents: ", error); + }); + //最初のレコードをセットする + await db.collection("members").orderBy('createdAt', 'desc').limit(1) + .get() + .then(function(querySnapshot) { + querySnapshot.forEach(function(doc) { + setFirstRecord(doc.data()) + }); + }) + .catch(function(error) { + console.log("Error getting documents: ", error); + }); + } + initialData(); fetchData(); },[inputRef]); + async function handleNextPage () { + console.log("handleNextPage") + //console.log("currentfirstRecord",currentfirstRecord) + //console.log("currentlastRecord",currentlastRecord) + //次のページを取得する + const colRef = db.collection("members") + .orderBy('createdAt', 'desc') + .limit(LIMIT_COUNT) + .startAfter(currentlastRecord.createdAt); + const snapshots = await colRef.get(); + var docs = snapshots.docs.map(function (doc) { + return doc.data(); + }); + setList(docs) + } + async function handlePreviousPage () { + console.log("handlePreviousPage") + //前のページを取得する + const colRef = db.collection("members") + .orderBy('createdAt') + .limit(LIMIT_COUNT) + .startAfter(currentfirstRecord.createdAt); + const snapshots = await colRef.get(); + var docs = snapshots.docs.map(function (doc) { + return doc.data(); + }); + //sort処理 + docs.sort(function(a,b){ + if(a.createdAt>b.createdAt) return -1; + if(a.createdAt < b.createdAt) return 1; + return 0; + }); + setList(docs) + } return ( <> <Typography variant="h4" align="center" component="h1" gutterBottom> 一覧表示 </Typography> <TableContainer component={Paper}> <Table className={classes.table} aria-label="simple table"> <TableHead> <TableRow> <TableCell>ID</TableCell> <TableCell>Email</TableCell> <TableCell>ハンドル名</TableCell> <TableCell>作成日</TableCell> <TableCell>操作</TableCell> </TableRow> </TableHead> <TableBody> { list.map((item, index) => { return ( <TableRow key={item.docId}> <TableCell component="th" scope="row"> {item.docId} </TableCell> <TableCell >{item.email}</TableCell> <TableCell >{item.displayName}</TableCell> <TableCell > { moment(item.createdAt.seconds * 1000).format('YYYY-MM-DD HH:mm:ss')} </TableCell> <TableCell ><Link to={`./edit/${item.docId}`}>編集</Link></TableCell> </TableRow> ) }) } </TableBody> </Table> </TableContainer> + <div variant="subtitle1" align="center" justify="center" component="h1" > + {hasPreviousPage && <><Fab component="span" className={classes.button} onClick={handlePreviousPage} ><NavigateBefore className={classes.largeIcon} color={"primary"} /></Fab> </>} + {!hasPreviousPage && <><Fab component="span" className={classes.button}><NavigateBefore className={classes.largeIcon} color={"disabled"}/></Fab> </>} + {hasNextPage && <><Fab component="span" className={classes.button} onClick={handleNextPage} ><NavigateNext className={classes.largeIcon} color={"primary"} /></Fab></>} + {!hasNextPage && <><Fab component="span" className={classes.button}><NavigateNext className={classes.largeIcon} color={"disabled"} /></Fab></>} + </div> <Typography className={classes.subtitle2} variant="subtitle2"><Link to="/screens/edit">新規作成</Link></Typography> <Typography className={classes.subtitle2} variant="subtitle2"><Link to="/">Homeに戻る</Link></Typography> </> ); }
プログラム解説1
初期読み込みの時に3つのデータを取得しています
- 1ページのデータ
- 最初のレコードを取得する
- 最後のレコードを取得する
例えば
- 1,2,3,4,5,6,7,8,9,10というデータ
- 1ページに3レコード
上記の場合は
- 1ページのデータ => 1,2,3のデータ
- 最初のレコードを取得する => 1のデータ
- 最後のレコードを取得する => 10のデータ
になります
const [firstRecord, setFirstRecord] = useState() const [lastRecord, setLastRecord] = useState() useEffect(() => { async function fetchData() { // featchDataという関数を定義し、それにasyncをつける //最初のページを取得する const colRef = db.collection("members") .orderBy('createdAt', 'desc') .limit(LIMIT_COUNT); console.log(colRef) const snapshots = await colRef.get(); var docs = snapshots.docs.map(function (doc) { return doc.data(); }); setList(docs) } async function initialData() { // featchDataという関数を定義し、それにasyncをつける //最後のレコードをセットする await db.collection("members").orderBy('createdAt', 'desc').limitToLast(1) .get() .then(function(querySnapshot) { querySnapshot.forEach(function(doc) { setLastRecord(doc.data()) }); }) .catch(function(error) { console.log("Error getting documents: ", error); }); //最初のレコードをセットする await db.collection("members").orderBy('createdAt', 'desc').limit(1) .get() .then(function(querySnapshot) { querySnapshot.forEach(function(doc) { setFirstRecord(doc.data()) }); }) .catch(function(error) { console.log("Error getting documents: ", error); }); } initialData(); fetchData(); },[inputRef]);
初期読み込みはいつもどおり「useEffect」の関数を使っています。
プログラム解説2
- 次と前のページのリンクを表示するのかどうかを判定しています。
- 現在表示しているページの最初のレコードと最後のレコードを取得しています。
- この処理はデータ(firstRecord,lastRecord,list)をいれたあとに起動(hook)する形になっています。つまりページ制御してデータを読み込むごとに実行しています
const [currentfirstRecord, setCurrentFirstRecord] = useState() const [currentlastRecord, setCurrentLastRecord] = useState() const [hasPreviousPage, sethasPreviousPage] = useState() const [hasNextPage, sethasNextPage] = useState() useEffect(() => { if (lastRecord && list) { if (list.length > 0 ){ sethasNextPage(true) list.forEach((item) => { //console.log("useEffect:list.item", item.docId) if ( item.docId === lastRecord.docId){ console.log("useEffect:lastRecord _ari", lastRecord) sethasNextPage(false) } }) //最後のレコードを取得する setCurrentLastRecord(list.slice(-1)[0]); } } if (firstRecord && list) { if (list.length > 0 ){ //console.log("useEffect:list",list) //console.log("useEffect:firstRecord",firstRecord) sethasPreviousPage(true) list.forEach((item) => { //console.log("useEffect:list.item", item.docId) if ( item.docId === firstRecord.docId){ console.log("useEffect:firstRecord _ari", firstRecord) sethasPreviousPage(false) } }) } //最初のレコードを取得する setCurrentFirstRecord(list[0]); } },[firstRecord,lastRecord,list]);
プログラム解説3
次のページを取得する処理になります。 startAfterというメソッドを使ってレコードを取得する範囲を設定している。設定する値は 現在、表示している最後のレコードになっています
async function handleNextPage () { console.log("handleNextPage") //console.log("currentfirstRecord",currentfirstRecord) //console.log("currentlastRecord",currentlastRecord) //次のページを取得する const colRef = db.collection("members") .orderBy('createdAt', 'desc') .limit(LIMIT_COUNT) .startAfter(currentlastRecord.createdAt); const snapshots = await colRef.get(); var docs = snapshots.docs.map(function (doc) { return doc.data(); }); setList(docs) }
プログラム解説4
前のページを取得する処理になります。
async function handlePreviousPage () { console.log("handlePreviousPage") //console.log("currentfirstRecord",currentfirstRecord) //console.log("currentlastRecord",currentlastRecord) //前のページを取得する const colRef = db.collection("members") .orderBy('createdAt') .limit(LIMIT_COUNT) .startAfter(currentfirstRecord.createdAt); const snapshots = await colRef.get(); var docs = snapshots.docs.map(function (doc) { return doc.data(); }); //sort処理 docs.sort(function(a,b){ if(a.createdAt>b.createdAt) return -1; if(a.createdAt < b.createdAt) return 1; return 0; }); setList(docs) }
他のデータ取得のプログラムと違うのはソート条件が違うという事です。
次ページ、1ページ取得処理
=> .orderBy('createdAt', 'desc')
前ページ取得処理
=> .orderBy('createdAt')取得したデータを逆ソートしています。
//sort処理 docs.sort(function(a,b){ if(a.createdAt>b.createdAt) return -1; if(a.createdAt < b.createdAt) return 1; return 0; });
- 例えば
1,2,3,4,5,6,7,8,9,10のデータがある時に、 currentのデータの範囲が4,5,6だった。
現在、表示している最初のレコードは4になる。
4のレコードを基軸にデータを取得する。
取得したデータは3,2,1になる。 そのデータを逆転ソートをすることで
1,2,3になって正しく表示する事ができる
プログラム解説5
次前のボタンの所です。ボタンはfabを使っています。
<div variant="subtitle1" align="center" justify="center" component="h1" > {hasPreviousPage && <><Fab component="span" className={classes.button} onClick={handlePreviousPage} ><NavigateBefore className={classes.largeIcon} color={"primary"} /></Fab> </>} {!hasPreviousPage && <><Fab component="span" className={classes.button}><NavigateBefore className={classes.largeIcon} color={"disabled"}/></Fab> </>} {hasNextPage && <><Fab component="span" className={classes.button} onClick={handleNextPage} ><NavigateNext className={classes.largeIcon} color={"primary"} /></Fab></>} {!hasNextPage && <><Fab component="span" className={classes.button}><NavigateNext className={classes.largeIcon} color={"disabled"} /></Fab></>} </div>
次前のボタンの出し分けをしている以外は難しい所はありません
ハマった所
- 初期で3回ほどデータを取りに行かないといけないのですが、callback関数をつないで作っていたのですが、データのセットまでにタイムラグがあってうまくいきませんでした。最終的にはすべてのデータをuseEffectで監視する事でうまくいきました。全部のデータが揃った時に動かすという処理ができてよかったです
感想
- mysqlのなら楽勝なのにoffsetが使えないとかマジきつい
- レコード数だけはとれるようにしてほしい。あわよくば現在のページかな。