web
react-infinite-scrollerを使ってFirestoreで無限スクロールを実装する
公開日:2022年1月25日
最終更新日:2022年1月25日
Firestoreでデータを追加読み込みする時に、無限スクロールにするためにreact-infinite-scrollerというライブラリを使ってみました。
https://github.com/danbovey/react-infinite-scroller
react-infinite-scroll-componentというライブラリもあるようですが、react-infinite-scrollerの方がGithubスターの数が多いので、今回はreact-infinite-scrollerを選びました。
https://github.com/ankeetmaini/react-infinite-scroll-component
インストール
パッケージを追加します
yarn add react-infinite-scroller
基本的な使い方
リストをInfiniteScrollコンポーネントで囲うだけです。スクロールしてリストの最後に差し掛かる時にloadmoreの関数が発火します。
import InfiniteScroll from "react-infinite-scroller"
// 省略
<InfiniteScroll
loadMore={loadMore}
hasMore={true}
loader={<div key={0}>loading</div>}
>
<ul>
{list.map((value) => <li>{value}</li>)}
</ul>
</InfiniteScroll>
Firestoreで追加読み込み
Firestoreではoffsetがないので、追加読み込み時はtimestampを使って読み込みした最後のリスト移行から追加読み込みするようにします。
やること
例えば15件ずつ取得する時にやることをざっと書き出すと...
- まずは普通に15件取得
- 取得した15件のリストの最後のtimestampをstateに保存
- 全てのリストのうち最後の1件のidを取得しておく(追加読み込みするかどうかを判定するため)
- react-infinite-scrollerでloadmoreが発火
- 2で保存したtimestampの次のリストから15件を取得する(startAfterを使う)
- 現在のリストと5で取得したリストを結合
- 上記を繰り返す
- リストの最後のidと3で取得したidが合致する場合は、これ以上ないと判断し追加読み込みはしない
実際のコード
いろいろ省略して雑ですがこんな感じです
const [lastDate, setLastDate] = useState(null)
const [todoList, setTodoList] = useState<TodoListItem[]>([])
useEffect(() => {
getLast()
getTodos()
}, [])
// 最後のidを取得
const getLast = async () => {
const res = await getDocs(
query(
collection(db, 'users', uid, 'todos'),
orderBy('createdAt', 'asc')
.limit(1)
)
)
setOldestId(res.docs[0].id)
}
// リスト取得
const getTodos = async () => {
let fetchTodos = await getDocs(
query(
collection(db, 'users', uid, 'todos'),
orderBy('createdAt', 'desc')
)
)
// lastDateがある場合は初回読み込みではないと判断しstartAfterで追加読み込みする
if (lastDate) {
// リストの最後のidと全てのリストの最後のidが同じ場合は追加読み込みしない
if (oldestId === todoList[todoList.length - 1].id) {
return
}
fetchTodos = fetchTodos.startAfter(lastDate)
}
const res = await fetchTodos.limit(15).get()
const todos: TodoListItem[] = res.docs.reduce(
(acc: any, doc: any) => [
...acc,
{
id: doc.id,
text: doc.data().text,
},
],
todoList
)
// リストのstateに生成したリストを追加
setTodoList(todos)
// 新しく取得したリストのうち最後のtimestampを保存
setLastDate(res.docs[res.docs.length - 1].data().createdAt)
setIsLoading(false)
}
<InfiniteScroll
loadMore={getTodos}
hasMore={oldestId !== todoList[todoList.length - 1].id}
>
<Timeline className={styles.styledTimeline}>
{todoList.map((item) => {
return (
<Timeline.Item key={item.id}>
<div>
{item.text}
</div>
</Timeline.Item>
)
})}
</Timeline>
</InfiniteScroll>