背景
個人アプリを作っている中でFirestoreからランダムで5個だけデータを取得する実装をしたかったのですが、調査を進めていくとそのような処理はFirebase側では用意されていないようなので、参考資料を元にReact, Firebaseで実装しました。
実装までに躓いたことを主にコードの解説をしていきます。
実装したコード
import { useEffect, useState } from "react";
import { projectFirestore } from "../firebase/config";
import { ProductDoc } from "../@types/stripe";
import { useCookies } from "react-cookie";
export const useRandomDocument = () => {
  const [documents, setDocuments] = useState<Array<ProductDoc>>([]);
  const [cookies] = useCookies(["random"]);
  const randomIndex = Number(cookies.random);
  useEffect(() => {
    let indexs: Array<string> = [];
    let randomDocument: Array<ProductDoc> = [];
    async function handleAsync() {
      while (randomDocument.length < 5) {
        const queryIndex = String(Math.floor(Math.random() * randomIndex + 1)); // DBの中に格納されている商品数以下の数字をランダムで出力する
        if (!indexs.includes(queryIndex)) {
          indexs = [...indexs, queryIndex];
          const productsRef = await projectFirestore.collection("products");
          const snapshot = await productsRef
            .orderBy("metadata.random")
            .startAt(queryIndex)
            .endAt(queryIndex)
            .get(); // startAtとendAtを同一に指定することでユニークな結果を出力できる
          const data = snapshot.docs.map((doc) => {
            return { ...(doc.data() as ProductDoc), id: doc.id };
          });
          randomDocument = [...randomDocument, ...data];
          if (randomDocument.length === 5) {
            setDocuments(randomDocument);
          }
        }
      }
    }
    handleAsync();
  }, [randomIndex]);
  return { documents };
};
今回の味噌
該当ページに遷移したらuseRandomDocumentを発火させます。
Firestoreからランダムに1個データを取得する処理を5個データがrandomDocumentに溜まるまで繰り返し条件を満たしたらuseStateに入れて処理を終了します。
■あらかじめ取得するデータにユニークな値randomをつける
あらかじめ取得するデータにrandom:<数字>を持たせる必要があります。
この下準備をしておくことで後述するstartAt(queryIndex)とendAt(queryIndex)を同じ値で指定して1個のみ取得することができます。
■数値をランダムで生成する
先に述べたようにFirestoreからランダムで値を取得できないのでランダムの処理は下記で再現しています。
const queryIndex = String(Math.floor(Math.random() * randomIndex + 1)); 
■between条件でクエリしてデータを取得する
const snapshot = await productsRef
            .orderBy("metadata.random")
            .startAt(queryIndex)
            .endAt(queryIndex)
            .get();
「あらかじめ取得するデータにつけたrandom」と「ランダムで生成した数値」が合致するデータを取得します。
本来ならstartAt(2月1日)とendAt(2月28日)にして1ヶ月の値をまとめてとってくるのがユースケースらしいですが、startAtとendAtの引数を同じ値にすることで1個だけデータを取得できます。
特定のデータを1個取得するならcollection().doc()の印象でしたがstartAtとendAtの方法もあるのですね。目から鱗でした。
さいごに
ということで、Firestoreから値をランダムで取得する方法について述べてきましたが、(今回の場合は)5回Firestoreへアクセスするのでパフォーマンスは落ちますし、その際のLoadingなどのUI・UX対策は必須になります。
もし他に良い方法があればご教示いただけますと幸いです。
参考URL