Querying Collections

This page covers how to query Firestore collections, and various techniques to get the best results from the library.

Providing a query reference

To start querying a collection, you need to provide the hooks with a CollectionReference or Query reference created directly from the firebase/firestore library. The reference can be a simple collection pointer or a fully constrained query using the querying functionality provided by the Firestore SDK.

For example, to create a query to a products collection:

import { collection } from "firebase/firestore";
import { firestore } from "./firebase";

const ref = collection(firestore, "products");

Or if you wish to provide query constaints:

import { collection, query, limit, where } from "firebase/firestore";
import { firestore } from "./firebase";

const ref = query(
  collection(firestore, "products"),
  limit(10),
  where("state", "==", "active")
);

Basic example

With your query reference in hand, you can get started.

The library provides 2 main hooks for querying collections; useFirestoreQuery and useFirestoreQueryData. Each hook is similar however returns slightly different results (see the next section for more detail). Within a component, import and provide a hook a Query Key and the reference:

import React from "react";
import { useFirestoreQuery } from "@react-query-firebase/firestore";
import {
  query,
  collection,
  limit,
  QuerySnapshot,
  DocumentData,
} from "firebase/firestore";
import { firestore } from "../firebase";

function Products() {
  // Define a query reference using the Firebase SDK
  const ref = query(collection(firestore, "products"));

  // Provide the query to the hook
  const query = useFirestoreQuery(["products"], ref);

  if (query.isLoading) {
    return <div>Loading...</div>;
  }

  const snapshot = query.data;

  return snapshot.docs.map((docSnapshot) => {
    const data = docSnapshot.data();

    return <div key={docSnapshot.id}>{data.name}</div>;
  });
}

The hook returns an instance of useQuery containing a QuerySnapshot. By using the useQuery hook you're able to reactivly handle events such as loading, errors, success, refetching and lots more.

If this doesn't make much sense to you, please check out the React Query documentation before proceeding!

Realtime updates

A powerful feature of Firestore is being able to easily subscribe to changes on the database. Luckily, the hooks handle this by providing a subscribe flag to the hook options:

const query = useFirestoreQuery(["products"], ref, {
  subscribe: true,
});

If data is changed which touches your provided query, the hook data will be updated.

Anytime the Query Key changes or your component unmounts or fresh data is required, the hook will automatically handle unsubscribing from new events.

Snapshots & Data

When Firestore returns data from a query, it does not provide the raw document data - instead we are provided a QuerySnapshot containing an array of DocumentSnapshot instances. Although these classes provide useful functionality, we sometimes just want the data from the database for convinience.

As shown above the useFirestoreQuery hook returns a QuerySnapshot, which requires developers to iterate over DocumentSnapshot instances and extract the data:

const query = useFirestoreQuery(["products"], ref);

const snapshot = query.data;

return snapshot.docs.map((docSnapshot) => {
  const data = docSnapshot.data();
  return <div key={docSnapshot.id}>{data.name}</div>;
});

If you are only interested in the data, the useFirestoreQueryData hook returns exactly this:

const query = useFirestoreQueryData(["products"], ref);

return query.data.map((document) => {
  return <div key={document.id}>{document.name}</div>;
});

Query Keys & References

A core concept of this library is the principles of React Query. React Query will refetch your data based on the default (or your own) configuration and when you update a Query Key.

If your query changes, ensure your Query Key also changes to reflect this. For example, the following will not work:

// This does not work!!!!!11
const [type, setType] = useState("swag");
const ref = query(collection, where("type", "==", type));

const query = useFirestoreQueryData(["products"], ref);

<button onClick={() => setType("merch")}>Update product type</button>;

Only when React Query performs a refetch will your data change, making your application state unpredictable.

To predicitably update your data, ensure your Query Key reflects your reference:

// This will work!!
const [type, setType] = useState("swag");
const ref = query(collection, where("type", "==", type));

// The Query Key now reflects the query constraints
const query = useFirestoreQueryData(["products", { type }], ref);

<button onClick={() => setType("merch")}>Update product type</button>;

Alongside being predictable, this now benefits from being an individual query which is cached. To learn more, read about Query Keys.

Cached / Server Data

When querying a collection without subscribing to changes, you can specify a source parameter to indiciate whether the cached or server documents should be queried:

useFirestoreQuery(["products"], query, {
  subscribe: false, // or undefined
  source: "cache", // or 'server'
});

If no value is provided, a combination of cache and server will be used (as default by Firestore).

Include metadata changes

If subscribing to a query, you can also subscribe to metadata changes on documents.

useFirestoreQuery(["products"], query, {
  subscribe: true,
  includeMetadataChanges: true,
});

Named Queries

If you have the ability to create data bundles, you can instead provide a named query to the hook. The hook will first download the query and then cache it based on the Firestore instance and bundle name.

import { loadBundle } from "firebase/firestore";
import { firestore } from "../firebase";

// Somewhere in your app...
const resp = await fetch("/bundles/products");

// Load in the bundle (named "products")
await loadBundle(firestore, bundle.body);

Elsewhere in your application, provide a named query function instead of a query:

import { useFirestoreQuery, namedQuery } from "@react-query-firebase/firestore";
import { firestore } from "../firebase";

function Products() {
  const query = useFirestoreQuery(
    ["products"],
    firestore,
    namedQuery(firestore, "products")
  );
}

If the bundle does not exist an error will be thrown.

Paginated / Infinite Queries

Paginating data (or loading infinite data) is supported via the useFirestoreInfiniteQuery and useFirestoreInfiniteQueryData hooks. The hooks both wrap the useInfiniteQuery hook.

The first step is to define an initial query, which will be called when the hook mounts:

const postsCollection = collection(firestore, "posts");
const postsQuery = query(postsCollection, limit(20));

const posts = useFirestoreInfiniteQuery("posts", postsQuery, () => {});

The 3rd argument accepts a callback, which is used to query the next page of data when requested. The callback is provided a QuerySnapshot (or array of document data), allowing you to construct a query to get the next batch of documents. If you return undefined, this will signal that there is no more data left to fetch.

const postsCollection = collection(firestore, "posts");
const postsQuery = query(postsCollection, limit(20));

const posts = useFirestoreInfiniteQuery("posts", postsQuery, (snapshot) => {
  const lastDocument = snapshot.docs[snapshot.docs.length - 1];

  // Get the next 20 documents starting after the last document fetched.
  return query(postsQuery, startAfter(lastDocument));
});

Within your application, call the fetchNextPage function to get the next page of data. This could be when the user presses a "Next Page" button or when they scroll to the bottom of the current list - it's up to you!

const posts = useFirestoreInfiniteQuery(...);

<button
  disabled={posts.isLoading}
  onClick={() => posts.fetchNextPage()}
>
  Load More
</button>

To learn more, visit the React Query Infinite Queries guide.

Modifying data responses

The library allows the passing of all React Query hook options to the library hooks. One such option is a select function which allows modification of data before it is returned from the query.

This functionality is really powerful. For example if we wished to return an array of product IDs rather than the entire document as the query result:

const query = useFirestoreQuery(
  ["products"],
  ref,
  {
    subscribe: true,
  },
  {
    // React Query data selector
    select(snapshot) {
      return snapshot.docs.map((docSnapshot) => docSnapshot.id);
    },
  }
);

const data = query.data; // ['id1', 'id2', ...]

Another usecase is returning a single document from a query when you do not know the ID:

// Users can only have a single active subscription
const findActiveSubscription = query(
  collection(firestore, "subscriptions"),
  where("uid", "==", "123"),
  where("status", "==", "active"),
  limit(1)
);

// Override the return type generic:
const query = useFirestoreQuery(
  "subscription",
  findActiveSubscription,
  {
    subscribe: true,
  },
  {
    select(snapshot) {
      if (snapshot.empty) {
        return null;
      }

      return snapshot.docs[0];
    },
  }
);

// Data will contain a single document or null.
if (query.data) {
  console.log("Your subscription: ", query.data);
}

Document IDs

When using the useFirestoreQuery & useFirestoreInfiniteQuery hooks, the data returned contains a QuerySnapshot, containing an array of DocumentSnapshot instances. These instances allow you to fetch both the data and metadata such as the documents ID.

The useFirestoreQueryData & useFirestoreInfiniteQueryData hooks instead returns an array of your document data, which does not include the document ID. If you'd like to return the document ID alongside the data, provide the idField option to the hook:

const products = useFirestoreQueryData("products", query, {
  idField: "_id",
});

if (products.isSuccess) {
  products.data.forEach((product) => {
    console.log("Product ID: ", product._id);
  });
}

Note the idField will override any data properties if they collide with the same name.

React Query options

Each hook allows us to provide useQuery options as the last argument, opening up the possibility to integrate the library easily across any application.

For example, we can handle side effects alonside reactive updates with ease:

const query = useFirestoreQuery(
  ["products"],
  ref,
  {
    subscribe: true,
  },
  {
    onSuccess(snapshot) {
      toast.success("Data loaded successfully!");
    },
    onError(error) {
      toast.error("Woops, something went wrong!");
    },
  }
);

if (query.isError) {
  return <Error error={query.error} />;
}

if (query.isSuccess) {
  return <ProductList data={query.data} />;
}