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} />;
}