DEV Community

Cover image for React Hooks with Firebase Firestore
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

React Hooks with Firebase Firestore

Written by Brian De Sousa✏️

What do React Hooks and Firebase have in common? They both accelerate development and reduce the amount of code you need to write to build something that would otherwise be complex.

It is actually quite incredible how fast you can put together a web app with data persistence when you couple the power and simplicity of Firestore with simple, efficient React function components and Hooks.

How do Hooks accelerate development?

First, a quick refresher on React Hooks. Hooks allow you to define stateful logic as reusable functions that can be used throughout your React application. Hooks also enable function components to tie into the component lifecycle, previously only possible with class components.

When it comes to creating components that need to handle lifecycle events, React does not prescribe whether you should use function components and Hooks or more traditional class components.

That being said, function components and Hooks have quickly become a big hit in the React developer community — and with good reason. Function components and Hooks greatly reduce the amount code and verbosity of a React app compared to class components.

LogRocket Free Trial Banner

How does Firestore accelerate development?

Firebase is a collection of services and tools that developers can piece together to quickly create web and mobile applications with advanced capabilities. Firebase services run on top of the Google Cloud Platform, which translates to a high level of reliability and scalability.

Firestore is one of the services included in Firebase. Firestore is a cloud-based, scalable, NoSQL document database. One of its most notable features is its ability to easily stream changes to your data to your web and mobile apps in real time. You will see this in action shortly in a sample app.

Web app development is further accelerated by the Firestore authentication and security rules model. The Firestore web API allows your web app to interact with your Firestore database directly from the browser without requiring server-side configuration or code. It’s literally as simple as setting up a Firebase project, integrating the API into client-side JavaScript code, and then reading and writing data.

React function components, Hooks, and the Firestore web API complement each other incredibly well. It’s time for to see all of these in action. Let’s take a look at an example grocery list web app and some of its code.

The grocery list web app

To explore using React Hooks with Firebase, we need some sample code. Let’s use the grocery list web app as an example.

Our Grocery List Web App
Grocery list web app.

You can try the grocery list web app for yourself. Please ignore the CSS styles resurrected from a 1990s website graveyard — UI design is clearly not my strong suit.

If you haven’t tried the app out yet, you might be wondering how it works. It allows you to create a new grocery list. The grocery list’s URL can be shared with other users, who can then join the list and add their own grocery items to the list.

Grocery list items immediately appear on the screen as they are added to the database. This creates a shared experience, where multiple users can add items to the list at the same time and see each other’s additions.

The grocery list web app is built completely using React function components and Hooks. Grocery list and user data is persisted to Firestore. The web app itself is hosted using Firebase hosting.

Full source code for the grocery list app is available on GitHub in the briandesousa/firebase-with-react-hooks repository.

Firebase web app configuration and initialization

All calls to the Firebase web API to retrieve or update data on Firestore have been grouped together in src/services/firestore.js. At the top of this file, you will see Firebase app initialization code that looks like this:

const firebaseConfig = {
    apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
    authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
    projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID
};
firebase.initializeApp(firebaseConfig);
const db = firebase.firestore();
Enter fullscreen mode Exit fullscreen mode

View the full source code here.

In order to use Firebase services, you must provide some configuration to the firebase.initializeApp function. The configuration you are required to provide depends on which Firebase services you are using.

In this case, I am only using Firestore, so an API key, authentication domain, and project ID are all that is required. Once you have created a Firebase project and added a web app, your unique configuration settings can be found on the General tab of the project’s settings screen on the Firebase console.

At first glance, the Firebase configuration settings seem as though they should be private and not exposed in the browser. That is not the case, though: they are safe to include in your client-side JavaScript. Your application is secured using Firebase authentication and Firestore security rules. I won’t get into those details here, but you can read more about it here.

You may have also noticed that I replaced configuration values with React environment variables defined on the global process.env object. You probably don’t want to include this configuration in your source code repository, especially if your repository is publicly available and intended to be shared and cloned by other developers.

Developers are bound to download your code and run it without realizing they are consuming your Firebase resources. Instead, I have opted to include a sample .env file that documents the configuration settings that must be provided before running the app. When I am running the app myself locally, I have my own .env.local file that doesn’t get checked into source control.

Writing data to Firestore

Once your Firebase configuration has been set up, getting started with writing to and reading data from your Firestore database requires very little code.

In its basic form, a Firestore database consists of collections of documents. A document can contain multiple fields of varying types, including a sub-collection type that allows you to nest document collections. All of this structure is generated on the fly as your JavaScript code makes calls to the Firebase API to write data.

For example, the following code creates a new grocery list document in the groceryLists collection:

export const createGroceryList = (userName) => {
    return db.collection('groceryLists')
        .add({
            created: firebase.firestore.FieldValue.serverTimestamp(),
            users: [{ name: userName}]
        });
};
Enter fullscreen mode Exit fullscreen mode

View the full source code here.

Initially, when a grocery list document is created, I only store the name of the user creating the list and a timestamp for when the list was created. When the user adds their first item to the list, an items sub-collection is created in the document to hold items on the grocery list.

The Firebase console’s database screen does a great job visualizing how your collections and documents are structured in Firestore:

Items Sub-collection Of A Document In Firestore
items sub-collection of a document in the groceryLists collection in Firestore.

Next, let’s look at how grocery list data is stored in React component state.

Managing grocery list state

React components can have state. Prior to hooks, if you wanted to use the React state API, your React components had to be class components. Now you can create a function component that uses the built-in useState Hook. In the grocery list web app, you’ll find an example of this in the App component.

function App() {
  const [user, setUser] = useState()
  const [groceryList, setGroceryList] = useState();
Enter fullscreen mode Exit fullscreen mode

View the full source code here.

The App component is the top-level component in the React component hierarchy of the grocery list web app. It holds onto the current user and grocery list in its state and shares that parts of that state with child components as necessary.

The useState Hook is fairly straightforward to understand and use. It accepts an optional parameter that defines the initial state to be used when an instance of the component is mounted (or, in other words, initialized).

It returns a pair of values, for which I have used destructuring assignment to create two local variables. For example, user lets the component access the current user state, which happens to be a string containing the user’s name. Then the setUser variable is a function that is used to update the user state with a new user name.

OK, great — the useState Hook lets us add state to our function components. Let’s go a little a deeper and look at how we can load an existing grocery list object from Firestore into the App component’s state as a side effect.

Loading state from Firestore as a side effect

When a link to a grocery list is shared with another user, that link’s URL identifies the grocery list using the listId query parameter. We will take a look at how we access that query parameter later, but first we want to see how to use it to load an existing grocery list from Firestore when the App component mounts.

Fetching data from the backend is a good example of a component side effect. This is where the built-in useEffect Hook comes into play. The useEffect Hook tells React to perform some action or “side effect” after a component has been rendered in the browser.

I want the App component to load first, fetch grocery list data from Firestore, and only display that data once it is available. This way, the user quickly sees something in the browser even if the Firestore call happens to be slow. This approach goes a long way toward improving the user’s perception of how fast the app loads in the browser.

Here is what the useEffect Hook looks like in the App component:

  useEffect(() => {
    if (groceryListId) {
      FirestoreService.getGroceryList(groceryListId)
        .then(groceryList => {
          if (groceryList.exists) {
            setError(null);
            setGroceryList(groceryList.data());
          } else {
            setError('grocery-list-not-found');
            setGroceryListId();
          }
        })
        .catch(() => setError('grocery-list-get-fail'));
    }s
  }, [groceryListId, setGroceryListId]);
Enter fullscreen mode Exit fullscreen mode

View the full source code here.

The useEffect Hook accepts two parameters. The first is a function that accepts no parameters and defines what the side effect actually does. I am using the getGroceryList function from the firestore.js script to wrap the call to the Firebase API to retrieve the grocery list object from Firestore.

The Firebase API returns a promise that resolves a DocumentSnapshot object that may or may not contain the grocery list depending on whether the list was found. If the promise rejects, I store an error code in the component’s state, which ultimately results in a friendly error message displayed on the screen.

The second parameter is an array of dependencies. Any props or state variables that are used in the function from the first parameter need to be listed as dependencies.

The side effect we just looked at loads a single instance of a document from Firestore, but what if we want to stream all changes to a document as it changes?

Streaming data in real time from Firestore as a side effect

React class components provide access to various lifecycle functions, like componentDidMount and componentWillUnmount. These functions are necessary if you want to do something like subscribe to a data stream returned from the Firestore web API after the component is mounted and unsubscribe (clean up) just before the component is unmounted.

This same functionality is possible in React function components with the useEffect Hook, which can optionally return a cleanup function that mimics componentWillUnmount. Let’s look at the side effect in the Itemlist component as an example:

    useEffect(() => {
        const unsubscribe = FirestoreService.streamGroceryListItems(groceryListId, {
            next: querySnapshot => {
                const updatedGroceryItems = 
                    querySnapshot.docs.map(docSnapshot => docSnapshot.data());
                setGroceryItems(updatedGroceryItems);
            },
            error: () => setError('grocery-list-item-get-fail')
        });
        return unsubscribe;
    }, [groceryListId, setGroceryItems]);
Enter fullscreen mode Exit fullscreen mode

View the full source code here.

The streamGrocerylistItems function is used to stream changes to the items sub-collection of a grocery list document as the data changes on Firestore. It takes an observer object and returns an unsubscribe function.

The observer object contains a next function that is called by the Firebase web API every time the items sub-collection changes. The unsubscribe function can be returned as is from the effect to stop streaming data from Firestore just before the ItemList component is unmounted. For example, when the user clicks the link to create a new grocery list, I want to stop the stream before displaying the create grocery list scene.

Let’s take a closer look at the streamGrocerylistItems function:

export const streamGroceryListItems = (groceryListId, observer) => {
    return db.collection('groceryLists')
        .doc(groceryListId)
        .collection('items')
        .orderBy('created')
        .onSnapshot(observer);
};
Enter fullscreen mode Exit fullscreen mode

View the full source code here.

The db variable is an instance of the Firestore type defined in the Firebase web API. The API lets you retrieve a single instance of a collection or document using the get function or stream updates to a collection or document using the onSnapshot function. The onSnapshot function receives the observer object and returns the unsubscribe function that we saw previously.

Next, let’s look at how we can create a custom Hook to encapsulate some shared state and logic.

Wrapping query string handling logic into a custom Hook

We want the grocery list app to use the list ID query parameter and react to changes to it. This is a great opportunity for a custom Hook that encapsulates the grocery list ID state and keeps it in sync with the value of the query parameter.

Here is the custom Hook:

function useQueryString(key) {
    const [ paramValue, setParamValue ] = useState(getQueryParamValue(key));

    const onSetValue = useCallback(
        newValue => {
            setParamValue(newValue);
            updateQueryStringWithoutReload(newValue ? `${key}=${newValue}` : '');
        },
        [key, setParamValue]
    );

    function getQueryParamValue(key) {
        return new URLSearchParams(window.location.search).get(key);
    }

    function updateQueryStringWithoutReload(queryString) {
        const { protocol, host, pathname } = window.location;
        const newUrl = `${protocol}//${host}${pathname}?${queryString}`;
        window.history.pushState({ path: newUrl }, '', newUrl);
    }
    return [paramValue, onSetValue];
}
Enter fullscreen mode Exit fullscreen mode

View the full source code here.

I have designed useQueryString as a generic Hook that can be reused to link together any state with any query parameter and keep the two in sync. The Hook has two internal functions that are used to get and set the query string parameter.

The getQueryParamValue function accepts the parameter’s name and retrieves its value. The updateQueryStringWithoutReload uses the browser history API to update the parameter’s value without causing the browser to reload. This is important because we want a seamless user experience without full page reloads when a new grocery list is created.

I use the useState Hook to store the grocery list ID in the Hook’s state. I return this state from the Hook in a way similar to how the built-in useState Hook works. However, instead of returning the standard setParamValue function, I return onSetValue, which acts as an interceptor that should only be called when the value of the state changes.

The onSetValue function itself is an instance of the built-in useCallback Hook. The useCallback Hook returns a memoized function that only gets called if one of its dependencies changes. Any props or state variables that are used by a useCallback hook must be included in the dependency array provided in the second parameter passed when creating the hook.

The end result is a custom Hook that initially sets its state based on a query parameter and updates that parameter when the state changes.

The useQueryParameter Hook is a highly reusable custom Hook. I can reuse it later on if I want to define a new type of state that I want to store in URL query string. The only caveat is that the state needs to be a primitive data type that can be converted to and from a string.

Recap and where to explore next

We have explored a few of the built-in React Hooks, such as useState, useEffect, and useCallback, but there are still others that could help you as you build your application. The React documentation covers all the built-in Hooks very clearly.

We have explored some of the Firebase web APIs that let you create, retrieve, and stream data from Firestore, but there are many other things you can do with the API. Try exploring the Firestore SDK documentation for yourself.

There are plenty of improvements that can be made to the grocery list web app, too. Try downloading the source code from GitHub and running it yourself. Don’t forget that you will need to create your own Firebase project and populate the .env file first before running the app. Clone or fork the repo and have fun with it!


Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

Alt Text

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — start monitoring for free.


The post React Hooks with Firebase Firestore appeared first on LogRocket Blog.

Top comments (0)