Using Firestore queries

In this chapter, we will use Firestore queries to scope songs to the current user, so when you sign in, you see your data.

Using where operator to filter documents

Let's begin by querying songs that belong to the current user. We'll need to add userId field to the document and update the form to set the current user id to the new documents.

First, let's change our songs hook and query songs by userId:

const [songs, setSongs] = useState();
useEffect(() => {
  if (user) {
    // 1. If the user is defined then perform the query
    return (
      firebase
        .firestore()
        .collection("songs")
        // 2. Query documents with userId equal the current user uid
        .where("userId", "==", user.uid)
        .onSnapshot(songs => setSongs(songs.docs))
    );
  } else if (songs) {
    // 3. If the user is signed out, clear the fetched songs
    setSongs(undefined);
  }
  // 4. Use the current user uid as the hook dependency
}, [user?.uid]);

We've added user?.uid as the hook dependency and a condition to the hook body, so we only perform the request when the user data is ready and clean up it when they sign out.

If you take a look at the app, you'll see that the data you entered so far is gone. That's because these documents have no userId. You can update it's using in the database viewer.

Update handleSubmit function and set userId to the new documents:

// 1. Add userId argument
async function handleSubmit(e, userId) {
  e.preventDefault();
  const form = e.target;

  const title = form.title.value;
  const rating = parseInt(form.rating.value);
  const firestore = firebase.firestore();

  // 2. Pass the argument to the add function
  firestore.collection("songs").add({ userId, title, rating });
  form.reset();
}

Finally, update the form onSubmit handler and pass user.uid:

<form name="song" onSubmit={handleSubmit}>
// →
<form name="song" onSubmit={e => handleSubmit(e, user.uid)}>

If you try to submit the form, you can see that everything works as expected!

Adding routing

Let's add a new feature to our app, the ability to share charts with the internet. But before we do that, we're going to make a detour.

So far, we had a single page, but now since we're adding another, it makes sense to introduce routing. For that we're going to use React Router. First, let's install it:

npm i --save react-router-dom
# or
yarn add react-router-dom

Now, let's rename our App component to Home and remove default export before it:

export default function App() {
// →
function Home() {

Instead we going to export a routing component as the default:

import firebase from "firebase/app";
import React, { useEffect, useState } from "react";
// 1. Import the router
import * as Router from "react-router-dom";
import "./App.css";

// 2. Add the root router component
export default function App() {
  return (
    <Router.BrowserRouter>
      <Router.Switch>
        <Router.Route exact path="/">
          <Home />
        </Router.Route>

        <Router.Route path="/p/:profileId" children={<Profile />} />
      </Router.Switch>
    </Router.BrowserRouter>
  );
}

// The rest of the code...

Now, let's add a link to the public profile that users can share:

<div>
  Signed in as {user.email} |{" "}
  <button onClick={() => firebase.auth().signOut()}>Sign out</button> |{" "}
  {/* Add the link to the profile */}
  <Router.Link to={`/p/${user.uid}`}>Share profile</Router.Link>
</div>

With that out of the way, we can back to adding the feature.

Ordering documents

We'll need to add a new component that will represent the profile. There we going to use a similar hook to the one we have in the Home component, but this time we're going to add order to the query:

function Profile() {
  const { profileId } = Router.useParams();

  const [songs, setSongs] = useState();
  useEffect(() => {
    return firebase
      .firestore()
      .collection("songs")
      .where("userId", "==", profileId)
      // Order songs by rating using the descending method,
      // so that the top-rated songs are on the top
      .orderBy("rating", "desc")
      .onSnapshot(songs => setSongs(songs.docs));
  }, [profileId]);

  return (
    <div className="App">
      <div>
        <Router.Link to="/">Create your own chart</Router.Link>
      </div>

      <ul>
        {songs
          ? songs.map(song => (
              <li key={song.id}>
                {song.data().title} {song.data().rating}
              </li>
            ))
          : "Loading..."}
      </ul>
    </div>
  );
}

If you would open the web app, you'll see that there's an exception FirebaseError: The query requires an index. The thing is that we've used a compound query, but didn't add an index for it. Firestore automatically creates indexes for every field and even nested objects and arrays. Still, since we queried by userId and ordered by rating, we have to add the index manually.

Luckily Firebase allows us quickly create the index. Simply follow the link provided in the exception, confirm the index creation, and wait for Firebase to finish the operation:

After you have done it, you can open the app and see that the top-rated songs are on the top.


We've learned how to use Firestore queries and create compound indexes. We added a new feature, the ability to share personal charts. Now it's time to secure our database, in the next chapter, we're going to learn how to use Firestore Security Rules and prevent authorized access.

Next chapter:
Security Rules (coming soon)
Learn how to secure your database and optimize data structure for easy reads.