Querying Documents

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

Providing a reference

To start querying a document, you need to provide the hooks with a DocumentReference created directly from the firebase/firestore library.

For example, to create a reference to a document in the products collection:

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

const collectionRef = collection(firestore, "products");
const ref = doc(collectionRef, "123");

Basic example

With your reference in hand, you can get started.

The library provides 2 main hooks for querying collections; useFirestoreDocument and useFirestoreDocumentData. 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 { useFirestoreDocument } from "@react-query-firebase/firestore";
import {
  query,
  collection,
  limit,
  QuerySnapshot,
  DocumentData,
} from "firebase/firestore";
import { firestore } from "../firebase";

function Product({ id }) {
  const ref = doc(firestore, "products", id);

  const product = useFirestoreDocument(["products", id], ref);

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

  const snapshot = product.data; // DocumentSnapshot

  return <div>{snapshot.data().name}</div>;
}

The hook returns an instance of useQuery containing a DocumentSnapshot. 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 product = useFirestoreDocument(["products", id], ref, {
  subscribe: true,
});

If data is changed which touches your document, 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 document, it does not provide the raw document data - instead we are provided a DocumentSnapshot instance. Although this can be useful, we sometimes just want the data from the database for convinience.

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

const product = useFirestoreDocumentData(["products", id], ref);

return <div>{product.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 reference changes, ensure your Query Key also changes to reflect this. For example, the following will not work:

// This does not work!!!!!11
function Product({ id }) {
  const collectionRef = collection(firestore, "products");
  const ref = doc(collectionRef, id);

  const product = useFirestoreDocument(["product"], ref);

  return ...
}

If the id prop changes, only the reference changes. The new ref would only be fetched when React Query refetches the data, making your application state unpredictable.

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

// This works!!
function Product({ id }) {
  const collectionRef = collection(firestore, "products");
  const ref = doc(collectionRef, id);

  // The Query Key now reflects the reference id.
  const product = useFirestoreDocument(["product", id], ref);

  return ...
}

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:

useFirestoreDocument(["products", id], ref, {
  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 document, you can also subscribe to metadata changes on documents.

useFirestoreDocument(["products", id], ref, {
  subscribe: true,
  includeMetadataChanges: true,
});

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 only a product name:

const query = useFirestoreDocument(
  ["products", id],
  ref,
  {
    subscribe: true,
  },
  {
    // React Query data selector
    select(snapshot) {
      return snapshot.exists() ? snapshot.data().name : null;
    },
  }
);

const data = query.data; // string | null

Your component will only-rerender when the name changes now!

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 = useFirestoreDocument(
  ["products", id],
  ref,
  {
    subscribe: true,
  },
  {
    onSuccess(snapshot) {
      toast.success("Document 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} />;
}