Querying Data

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

Providing a database reference#

To query data in your realtime database, you first need to create a DatabaseReference, which acts are the pointer for the location of the data you wish to query. The refernce is created from the firebase/database library. For example, to create a reference pointing at the products node:

import { ref } from "firebase/database";
import { database } from "./firebase";

const dbRef = ref(database, "products");

Basic example#

With your reference in hand, you can get started.

The library provides 2 main hooks for querying data; useDatabaseSnapshot and useDatabaseValue. Both are similar however return differing results from the query. The first returns a DataSnapshot containing your nodes data, metadata and various utility functions, whilst the second returns the raw value of the node (or null if it doesn't exist).

Lets return a snapshot containing all of our products using the useDatabaseSnapshot hook:

import { ref } from "firebase/database";
import { useDatabaseSnapshot } from "@react-query-firebase/database";
import { database } from "../firebase";

function ProductsList() {
  const dbRef = ref(database, "products");
  const products = useDatabaseSnapshot(["products"], dbRef);

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

  // DataSnapshot
  const snapshot = products.data;

  let children = [];

  // Iterate the values in order and add an element to the array
  snapshot.forEach((childSnapshot) => {
    children.push(
      <div key={childSnapshot.key}>Product: {childSnapshot.val().name}</div>
    );
  });

  return <div>{children}</div>;
}

The snapshot allows us to access many useful utilities, such as whether the node exists, has children, access a child node and more.

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 Realtime Data 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 products = useDatabaseSnapshot(["products"], dbRef, {
  subscribe: true,
});

If data is changed which touches your database node (or children of it), 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.

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 [productId, setProductId] = useState("1");
const dbRef = ref(database, `products/${productId}`);

const query = useDatabaseSnapshot(["product"], dbRef);

<button onClick={() => setType("2")}>Load new product</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 [productId, setProductId] = useState("1");
const dbRef = ref(database, `products/${productId}`);

const query = useDatabaseSnapshot(["product", productId], dbRef);

<button onClick={() => setType("2")}>Load new product</button>;

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

Using useDatabaseValue#

The useDatabaseValue returns the raw values of a node, rather than a DataSnapshot which can reduce boilerplate code when building your UI.

import { ref } from "firebase/database";
import { useDatabaseValue } from "@react-query-firebase/database";
import { database } from "../firebase";

function Product({ id }) {
  const dbRef = ref(database, `products/${id}`);
  const product = useDatabaseValue(["products", id], dbRef, {
    subscribe: true,
  });

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

  return <div>{product.data.name}</div>;
}

Handling lists of data#

The Realtime Database does not have a concept of "arrays", instead you are expected to store your list of data within an object, whereby the key of the data is a sentinial value representing the order at which it was added. You can see the list of ordered data for more information on this.

When using the useDatabaseValue hook when you expect a list of data, you may be tempted to iterate over the object values returned from the hook, however this does not guarentee order due to the way JavaScript works. The useDatabaseSnapshot hook returns an instance which provides a forEach method which allows you to iterate over your list in order.

To gain the same functionality with the useDatabaseValue hook, you can instead provide a toArray flag to the hook. If the returned value contains child nodes, the hook will return the values as an ordered array for you.

For example, let's assume you push new events to an audit trail of the user:

const auditRef = ref(db, "users/123/audit");

await set(push(auditRef), "Event 1");
await set(push(auditRef), "Event 2");
await set(push(auditRef), "Event 3");

The data will be stored in the database using special key values. Since we've structured our data whereby the users/123/audit node is an ordered list, we can provide the flag to ensure the list is returned in the correct order:

const id = "123";
const auditRef = ref(db, `users/${id}/audit`);
const audit = useDatabaseValue(["audit", id], auditRef, {
  toArray: true,
});

if (audit.isSuccess) {
  console.log(audit.data); // ['Event 1', 'Event 2', 'Event 3'];
}

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 products = useDatabaseSnapshot(
  ["products"],
  dbRef,
  {
    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} />;
}