FirebaseとReactでページネーション

はじめに

受注のあったアプリをFirebase+React+Typescriptで開発中です。
ページネーションと書きましたが、実際は次へと前へのボタン二つで実装です。
(※そして少々回りくどいかもしれません!!申し訳ありません!!)
Firebaseのクエリでは本物ページネーションが難しいため、開発中のアプリはAlgoliaを導入しました。
Firebase+Algoliaの開発記事はまた書こうと思います。

流れ

今回使うメインのデータの型

interface USER {
  uid: string
  name: string
  createdAt: Date
  image: string
}

今回用意するState
Stateの上が関連する説明として書いてます。

// fetchしたデータ、その時表示させる件数分しかsetしません
const  [ fetchData,  setFetchData ] = useState<USER[]>([]) // ①

// fetchDataで取得したデータの最初のtimestamp、ページを戻る場合に使う
// ちなみにtimestamp(createdAt)は、Firebaseだと下記のデータが入る
/*
createdAt: {
  nanoseconds: 34000000
  seconds: 1635892516
}
*/
const  [ firstTime,  setFirstTime ] = useState<Date>() // ②

// fetchDataで取得したデータの最後のtimestamp,ページを進む場合に使う
const  [ lastTime,  setLastTime ] = useState<Date>() // ③

// 全てのデータの最初のID
const  [ firstId, setFirstId ] = useState('') // ④

// 全てのデータの最後のID
const  [ lastId, setLastId ] = useState('') // ⑤

1.対象のデータをfetchして①のfirstTimeへsetする。
2.fetchしたデータの最初と最後のtimestamp(createdAt)をstateへset、最初の値を②のfirstTime、最後の値を③lastTimeへsetする。
3.fetchしたデータの最初と最後のID(uid)をstateへset、最初の値を④のfirstId、最後の値を⑤lastIdへsetする。
4.fetchしたデータに2でsetした最初と最後IDがあれば、ページを戻ったり進められないよう処理する。
※今回はフロント部分で矢印のボタンを制御するようにしました。
firstIdもlastIdも合致していない場合、どちらの矢印も表示させる f:id:zare926:20211129084607p:plain fetchDataの最初のデータのIDがfirstIdと合致している場合、戻る矢印は表示させない f:id:zare926:20211129084600p:plain fetchDataの最後のデータのIDがlastIdと合致している場合、進む矢印は表示させない f:id:zare926:20211129084631p:plain

tsxで書くとこんな感じ

{ fetchData[0].uid != firstId && 戻る矢印 }

{ fetchData[fetchData.length - 1].uid !=  lastId && 進む矢印 }

5.次に進む場合は、2でsetした③lastTimeの次のリストからデータをfetchする。
この時FirebaseのメソッドでstartAfterを使う。
6.前に戻る場合は、2でsetした②firstTimeの手前のリストからデータをfetchする。
この時fetchするデータをlimitで制限している場合、進むとは違って、limitToLastというメソッドとendBeforeを掛け合わせて使う。

という感じです。
下準備が多く、わかりにくく申し訳ありません…
これいらないんじゃないの?とか別の方法があれば教えてもらえたら幸いです!

コード

ざっくりとまとめるとこんな感じです。
今回fetchするデータはcollectionのusersを9件ずつ取得することにしてます。
フロント部分は最小限ですし、CSSも当たってないのでデザインしてください。

Pagination.tsx

import React, { useEffect, useState } from 'react'
import { db } from '../../../firebase' // ディレクトリ適当です、アプリごと変わります

interface USER {
  uid: string
  name: string
  createdAt: Date
  image: string
}

const Pagination = () => {
  const usersRef = db.collection('users')

  const [fetchData, setFetchData] = useState<USER[]>([])
  const [firstTime, setFirstTime] = useState<Date>()
  const [lastTime, setLastTime] = useState<Date>()
  const [firstId, setFirstId] = useState('')
  const [lastId, setLastId] = useState('')

  useEffect(() => {
    fetchUsersData()
  }, [])

  //  setStateをまとめてます
  const setMethod = (dataList: USER[]) => {
    setFirstTime(dataList[dataList.length - 1].createdAt)
    setLastTime(dataList[0].createdAt)
    setFirstId(dataList[dataList.length - 1].uid)
    setLastId(dataList[0].uid)
  }

  // 一番最初の9件を取りに行く
  const fetchUsersData = () => {
    let dataList: USER[] = []
    usersRef
      .limit(9)
      .orderBy('createdAt', 'desc')
      .get()
      .then((snapshot) => {
        snapshot.forEach((doc) => {
          const data = doc.data()
          const newData = {
            uid: data.uid,
            name: data.name,
            createdAt: data.createdAt,
            image: data.image,
          }
          dataList.push(newData)
        })
        setFetchData(dataList)
        setMethod(dataList)
      })
  }

  //  次のページ分を取りに行く
  // startAfterが指定したcreatedAt以降のデータを取ってきてくれる
  const fetchNextUsersData = () => {
    let dataList: USER[] = []
    usersRef
      .limit(9)
      .orderBy('createdAt', 'desc')
      .startAfter(lastTime)
      .get()
      .then((snapshot) => {
        snapshot.forEach((doc) => {
          const data = doc.data()
          const newData = {
            uid: data.uid,
            name: data.name,
            createdAt: data.createdAt,
            image: data.image,
          }
          dataList.push(newData)
        })
        setFetchData(dataList)
        setMethod(dataList)
      })
  }

  //  前のページ分を取りに行く
  // endBeforeとlimitToLastで指定したcreatedAtより手前のデータを取ってきてくれる
  const fetchPrevUsersData = () => {
    let dataList: USER[] = []
    usersRef
      .limitToLast(9)
      .orderBy('createdAt', 'desc')
      .endBefore(firstTime)
      .get()
      .then((snapshot) => {
        snapshot.forEach((doc) => {
          const data = doc.data()
          const newData = {
            uid: data.uid,
            name: data.name,
            createdAt: data.createdAt,
            image: data.image,
          }
          dataList.push(newData)
        })
        setFetchData(dataList)
        setMethod(dataList)
      })
  }
  return (
    <>
      {fetchData.map((fd: USER) => (
        <div>
          <img src={fd.image} />
        </div>
      ))}

      <div className='prevBtn'>
        {fetchData[0].uid != firstId && <img src={左矢印} onClick={() => fetchPrevUsersData()} />}
      </div>
      <div className='nextBtn'>
        {fetchData[fetchData.length - 1].uid != lastId && <img src={右矢印} onClick={() => fetchNextUsersData()} />}
      </div>
    </>
  )
}

export default Pagination


これでうまく機能すると思います!