react

Building custom React Hooks to fetch data from Firebase Firestore

WK

William Kurniawan

Aug 5, 2020

In this article, I want to share my experience of building custom React Hooks to fetch data from Firebase Firestore. I will assume that you already have a basic understanding of React Hooks. If you are entirely new to React Hooks, you can learn the basic of React Hooks in my previous article.

Why React Hooks?

A quick refresher on React Hooks, Hooks enable you to define stateful logic as reusable functions that can be used throughout your React application. Hooks also allow function components to bind into the component lifecycle, earlier only achievable with class components.
When it comes to building components that need to manage lifecycle events, React does not suggest whether you should use function components and Hooks or old-fashioned class components.
That being said, function components and Hooks have suddenly become a trending topic in the React developer community. Function components and Hooks significantly diminish the amount code and verbosity of a React app compared to class components.

Get started

To understand how to use React Hooks to fetch data from Firebase Firestore, we will build a simple function which will fetch a list of Instagram posts and listen to changes. You can bootstrap your project using create-react-app or create-next-app. I will also assume that you already understand how to add Firebase into your project dependencies and initialise it as this article will mainly focus on building the custom React Hooks.
First, open your project and create a new file called usePosts.ts.
function usePosts() {}

export default usePosts;
Custom Hooks are more of a convention than a feature. If a function's name starts with "use" and it calls other Hooks, we say it is a custom Hook. The useSomething naming convention is how our linter plugin can find bugs in the code using Hooks.
Open your newly created custom Hook and define the state to manage the data coming from Firestore. Update your custom Hook to look like this:
import React from 'react';

interface Data {
  error: Error | null;
  loading: boolean;
  posts: Post[];
}

interface Post {
  id: string;

  caption: string;
  imageURL: string;
  totalComments: number;
  totalLikes: number;
}

function usePosts() {
  const [data, setData] = React.useState<Data>({
    error: null,
    loading: true,
    posts: [],
  });

  return data;
}

export default usePosts;
At this stage, when we use our custom Hook, it will always return the default data as we haven't attached any function that will fetch data from Firestore. Without wasting more time, let's start writing our function to fetch data from Firestore. Update your custom Hook to look like this:
import React from 'react';
// Firebase
import firebase from '../firebase/client';

interface Data {
  error: Error | null;
  loading: boolean;
  posts: Post[];
}

interface Post {
  id: string;

  caption: string;
  imageURL: string;
  totalComments: number;
  totalLikes: number;
}

function usePosts() {
  const [data, setData] = React.useState<Data>({
    error: null,
    loading: true,
    posts: [],
  });

  React.useEffect(() => { // <--- 1
    const unsubscribe = firebase // <--- 2
      .firestore()
      .collection('posts')
      .onSnapshot(
        (snapshot) => {
          setData({
            error: null,
            loading: false,
            posts: snapshot.docs.map((doc) => ({
              id: doc.id,
              caption: doc.data().caption,
              imageURL: doc.data().imageURL,
              totalComments: doc.data().totalComments,
              totalLikes: doc.data().totalLikes,
            })),
          }); // <--- 3
        },
        (error) => {
          setData({
            error,
            loading: false,
            posts: [],
          }); // <---4
        },
      );

    return unsubscribe; // <--- 5
  }, []);

  return data;
}

export default usePosts;
Code explanations:
  1. We will use useEffect Hook to handle data fetching from Firestore. This useEffect Hook will be executed when our custom Hook is mounted.
  2. We create a Firestore listener (snapshot) to fetch and listen to data changes in posts collection. This onSnapshot function will return a function that can be used to unsubscribe from the data changes subscription.
  3. When we get the data returned from Firestore, we map the data and update our Hook state accordingly. We will set the loading state to false and posts to list of posts coming from Firestore. Any components that implement this usePosts Hook will get the state updates.
  4. When there is any issue (e.g. insufficient permission or network issue), Firestore will return an error object. Our custom Hook is responsible to update its state to reflect this. We will set the error state to the error object coming from Firestore and loading state to false. Any components that implement this usePosts Hook will get the state updates.
  5. When this usePosts Hook is unmounted, it will unsubscribe from future incoming data changes.
To use this usePosts Hook, you can simply call usePosts Hook in any function components.
import React from 'react';
// Hooks
import usePosts from '../hooks/usePosts';

const Component: React.FC = () => {
  const { error, loading, posts } = usePosts();

  console.log('error:', error);
  console.log('loading:', loading);
  console.log('posts:', posts);

  return <div>Hello, Custom Hook!</div>;
};

export default Component;

What's next?

We have explored a few of the built-in React Hooks, such as useState and useEffect, but there are still others that could help you as you develop your application. The React documentation explains all the built-in Hooks very clearly.
We have explored one of the Firebase web APIs that let you fetch and listen to data changes from Firestore, but there are many other things you can do with the API. Try exploring the Firebase documentation for a deeper understanding of Firebase APIs.

Copyright © 2020 William Kurniawan. All rights reserved.