Firestoreのページネーション/無限スクロール+スナップショットリスナー

18539 ワード

私は多くの開発者が彼らのアプリケーションで実装しようとしていることに気づいていました、そして、それはOnsnapshotリスナーによる反応的なページ化、すなわちバッチ/空白/ページのコレクションから文書をロードして、誰かが変化するとき、また、コレクションの文書を加えるか、削除するのを聞いていると気がつきました.
まず第一に、私が私のコードを共有する前に、これが多分あなたが一般的にしたいことでないと強調したいです.ファイアベースコンサルタントとして
Doug Stevenson said in one of his comments on stackoverflow posts

"You can't really combine paging with listening. You have to choose one or the other in order to maintain the sanity of your code."


トレードオフと利用事例を理解するのは良いことです.私はPageGrid/無限のスクロールを使用して大きなフィードでリアルタイムリスニングを使用してアプリケーションを考えることはできませんし、アイテムの編集と削除のリアルタイム動作のハンドル.チャットでも、メッセージを削除すると、その状態をremoved=true すぐにデータベースから削除しないでください.それはあなたのアプリケーションを維持して、持っている重いものの種類です、そして、それはあなたが実際に必要とするどの機能を評価するのによいです.
しかし、理論演習のために、それを実行しようとしましょう!
これは、あなたが設定しなければならないほど完全ではないstartAfter and endAt FireStoreカーソルを1つのバルク/バッチ/ページから別の1つの負荷を防ぐために.
したがって、2つのクエリが必要です.
文書の初期負荷のための1つlimit(PAGE_LIMIT) (まだ必要です)startAfter - それはquery 変数)
query.limit(PAGE_LIMIT).get()
との間のドキュメントの変更をリッスン別の1つstartAfter and endAt 変更startAfter であるquery 変数
query.endAt(newPosts[newPosts.length-1].timestamp).onSnapshot()
私は、あなたがあなたのポストの一時的なスケールを予測することができなくて、明示的なPageRain限界を無視しない限り、あなたがちょうど1つの質問でそれを作ることができると思いません.
以下にコードを示します:
import { useEffect, useRef, useState } from 'react'

const PAGE_LIMIT = 5;

const useInfiniteScroll = ({ fetching: fetchingInit = false, hasMore: hasMoreInit = false, threshold = 200 }) => {
  const [fetching, setFetching] = useState(fetchingInit);
  const [hasMore, setHasMore] = useState(hasMoreInit);

  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [fetching, hasMore]);

  function handleScroll() {
    const offsetHeight = document.documentElement.offsetHeight, innerHeight = window.innerHeight,
          scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;

    if (!hasMore || fetching || innerHeight + scrollTop + threshold <= offsetHeight) return;
    setFetching(true);
  }

  return [fetching, setFetching, setHasMore];
}

const PostFeed = ({}) => {
  const [pages, setPages] = useState([[]]),
        [fetching, setFetching, setHasMore] = useInfiniteScroll({ fetching: true, hasMore: true }),
        listeners = useRef([]);

  useEffect(() => () => listeners.current.map(l => l()), []);

  useEffect(() => {
    if (!fetching) return;
    const l = pages.length, page = pages[l - 1] || [], postLast = page[page.length - 1];
    let query = firebase.firestore().collection('posts').orderBy("timestamp", "desc");
    if (0 < l) query = query.startAfter(postLast?.timestamp || new Date(253402300799999));

    let mounted = true;
    query.limit(PAGE_LIMIT).get().then(snapshot => {
      if (mounted) {
        const posts = snapshot.docs.map(doc => doc.data());
        setPages(ps => ps.concat([posts]));
        const unsubscribe = query.endAt(posts[posts.length-1]?.timestamp || new Date()).onSnapshot(snapshot => {
          const postsUpdated = snapshot.docs.map(doc => doc.data());
          setPages(ps => ps.map((b, i) => i === l ? postsUpdated : b));
        });
        listeners.current = listeners.current.concat([unsubscribe]);
        if (posts.length < PAGE_LIMIT) setHasMore(false);
        setFetching(false);
      }
    });

    return () => mounted = false;
  }, [fetching]);

  return (
    <>
      {pages.map(page => page.map(post =>
        <PostCard key={post.id} post={post} />
      ))}
      { fetching && <SomePostSkeleton /> }
    </>
  )
}

export default PostFeed;
警告:
  • new Date(253402300799999) 可能な最大日付firestore Timestamp object そして、それは9999 - 12 - 31 T 23 : 59 : 59.9999999999 zです.我々は、同じクエリを再利用し、最初のページに新しい将来の記事を聞くためにこれを使用します.私はいくつかのものを変更し、最後のポイントを見て、私たちはもうそれを必要としない0 < l -> last(batch)?.[orderBy] が存在しなければならない.setHasMore であろうtrue そして、我々はセットすることができませんfetching to true ということになります.
  • useInfiniteScroll 伝統的ですuseInfiniteScroll フック、それについて特別な何も、多分私たちがその値を設定することに注意することは重要ですfetching すぐにPostFeed , なぜなら、最初の負荷に対して同じ問い合わせを使用しているからです.
  • ページの概念私は、前に用語一括/バッチを使用しました.この時点で私はこれらの'クラスタ'の名前を知らない.しかし、はい、あなたが気づくことができるように、私たちのPost Arrayは配列の配列です.
  • 変数l の範囲内で保存されますuseEffect したがって、バッチ/ページのドキュメントが更新されるたびに、変更は右のバッチ/ページにマップされます.
  • リスナーを取り消すことを忘れないでください.我々の店useRef だってuseState それがレンダリングに影響を与えて、他のすべてのリターン関数で一度にすべてを取り消すならば、使われるべきですuseEffect .
  • 使用するmounted 順序の応答を処理する変数.つまり、firestoreの約束は未定ですが、コンポーネントはアンマウントされています.約束が終了すると、アンマウントされたコンポーネントの状態を設定すると、警告が発生します.参照https://reactjs.org/docs/hooks-faq.html#how-can-i-do-data-fetching-with-hooks を検索してください.
  • また、使用endAt の代わりにendBefore だってstartAfter 前のバッチ/ページ(オープン間隔)の終了を除くendAt 現在のバッチ/ページ(閉じられた間隔)の終わりを含みます.
  • 使用するif (0 < l) 未使用の書類も含む条件timestamp プロパティ.私たちはすべてのドキュメントを持っているtimestamp プロパティ.実際には、ドキュメントを更新するとき、timestamp まだインデックスされていませんstartAfter - endAt 間隔とは、修正されたドキュメントの迷惑フリッカーを引き起こす可能性がありますので、我々は持っていないstartAfter 最初のバッチを追加するとき.
  • してください、任意の質問を聞いたり、発言を追加無料!私は、すべてをはっきりと説明することができなかったかもしれません.
    歓声と楽しみ