react-infinite-scrollerというライブラリを使ってみました。
react-infinite-scroll-componentというライブラリもあるようですが、react-infinite-scrollerの方がGithubスターの数が多いので、今回はreact-infinite-scrollerを選びました。
GitHub - ankeetmaini/react-infinite-scroll-component: An awesome Infinite Scroll component in react.
インストール
yarn add react-infinite-scroll-component
基本的な使い方
リストを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 [todoList, setTodoList] = useState<TodoListItem[]>([]) useEffect(() => { getLast() getTodos() }, []) // 最後のidを取得 const getLast = async () => { const res = await firebase .firestore() .collection('users') .doc(query.uid) .collection('todos') .orderBy('createdAt', 'asc') .limit(1) .get() setOldestId(res.docs[0].id) } // リスト取得 const getTodos = async () => { let fetchTodos = firebase .firestore() .collection('users') .doc(query.uid) .collection('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>