TutorialsUsing Firestore with Next.js

Learn how to start using Firebase Firestore in your Next.js and React application

·7 min read
Cover Image for Using Firestore with Next.js

Next.js is a serverless framework that can be used with any database you prefer, and it's one of the reasons why it's so popular among developers.

Why we use Firestore with Next.js

Firestore is a common choice among Next.js developers due to it being performant, scalable, simple, real-time, offline-first, and with a generous free tier. There's a lot to like.

Firestore is a serverless database that we can use from both the client and the server. Communicating with Firestore right from the browser is likely one of its biggest strengths: you no longer need an API to read and write to your database because it can be accessed directly via the SDK.

The possibility of not having to write an API is a huge time-saver for developers, and further help to front-end developers who want to write full-stack applications from scratch.

If you're wondering how we can keep our database secure despite being used from a client (good question!), you may want to take a look at this guide for writing Security Rules for your Firestore database.

One of the biggest criticisms of Firestore was the fact that it added a significant additional size to client-side bundles due to its heavy SDK.

Fortunately, The newly released version 9 of Firebase made massive improvements in terms of size, which can go even further when using the Lite version.

In this article, I want to introduce you to Firestore using Next.js at the time of writing one of the most popular React frameworks, and the framework we built MakerKit on.

Setting Up Firestore with Next.js

If you haven't yet configured your application, you may want to get started using our article about setting up the Firestore Emulators with Next.js.

We use reactfire; it's a Typescript library by Firebase designed to help developers use Firebase with any React codebase.

Firestore Provider

We need to initialize Firestore and write a provider to wrap the children that use the Firestore hooks.

The code below is a simplified version of what MakerKit does to initialize a Firestore instance:

import { FirestoreProvider, useFirebaseApp } from 'reactfire';
import { useMemo } from 'react';

import {
  enableIndexedDbPersistence,
  connectFirestoreEmulator,
  initializeFirestore,
} from 'firebase/firestore';

import { isBrowser } from '~/core/generic';

export default function FirebaseFirestoreProvider({
  children,
  useEmulator,
}: React.PropsWithChildren<{ useEmulator?: boolean }>) {
  const firestore = useFirestore();

  // connect to emulator if enabled
  if (useEmulator) {
    const host = getFirestoreHost();
    const port = Number(getFirestorePort());

    try {
     connectFirestoreEmulator(firestore, host, port);
    } catch (e) {
      // this may happen on re-renderings
    }
  }

  const enablePersistence = isBrowser();

  // We enable offline capabilities by caching Firestore in IndexedDB
  // NB: if you don't want to cache results, please remove the next few lines
  if (enablePersistence) {
    enableIndexedDbPersistence(firestore)
  }

  return <FirestoreProvider sdk={firestore}>{children}</FirestoreProvider>;
}

function getFirestoreHost() {
  return process.env.NEXT_PUBLIC_FIREBASE_EMULATOR_HOST ?? 'localhost';
}

function getFirestorePort() {
  return process.env.NEXT_PUBLIC_FIRESTORE_EMULATOR_PORT ?? 8080;
}

function useFirestore() {
  const app = useFirebaseApp();

  return useMemo(() => initializeFirestore(app, {}), [app]);
}

If we use the provider above in _app.tsx, every page can have access to Firestore. Adding the Firebase providers may not be the best solution depending on what your application does and how lightweight you want your pages.

Our first Firestore hook to read a document

The library Reactfire helps us with writing queries to our Firestore database thanks to a list of predefined React hooks.

For example, one common query in SaaS applications is to fetch the organization/project a user belongs to. In MakerKit, we wrote it similarly to the code below:

import { useFirestore, useFirestoreDocData } from 'reactfire';
import { doc, DocumentReference } from 'firebase/firestore';
import { Organization } from '~/lib/organizations/types/organization';

type Response = Organization & { id: string; };

export function useFetchOrganization(organizationId: string) {
  const firestore = useFirestore();
  const organizationsPath = `/organizations`;

  const ref = doc(
    firestore,
    organizationsPath,
    organizationId
  ) as DocumentReference<Response>;

  return useFirestoreDocData(ref, { idField: 'id' });
}

We imported the Firestore instance using the React hook useFirestore. By using the function doc we can get a reference to a Firestore document at the path specified. In this case, we're fetching a reference of the path /organizations/${organizationId}.

Using the hook useFirestoreDocData, we can directly query and retrieve the document's data.

By adding the property idField, we're also adding the id of the document to the data object. Adding the id field is the reason why we merge the interface of the response with the interface { id: string }.

Reading a list of documents using Firestore

To read a list of documents from a Firestore collection, we can use the useFirestoreCollectionData hook.

import { useFirestore, useFirestoreCollectionData } from 'reactfire';
import { collection, CollectionReference, where, query } from
'firebase/firestore';

export function useFetchUserOrganizations(userId: string) {
  const firestore = useFirestore();
  const organizationsPath = `organizations`;

  const ref = collection(
    firestore,
    organizationsPath
  ) as CollectionReference<Organization>;

  const userPath = `members.${userId}`;
  const operator = '!=';

  // adding the constraint where. This means: retrieve the organizations
  // where the field members[userId] is not null, eg: it exists
  const constraint = where(userPath, operator, null);
  const organizationsQuery = query(ref, constraint);

  return useFirestoreCollectionData(organizationsQuery);
}

Using document queries in Firestore

Reactfire's hooks are async and therefore re-render based on the status of the hook, which can be one of the following: loading, error, or success.

const Organization: React.FC<{
  id: string
}> = ({ id }) => {
  const { data: organization, status }
    = useFetchOrganization(id);

  // warn users there was an error
  if (status === 'error') {
    return <p>Ops! Error encountered while loading organization data...</p>
  }

  // show a loading indicator
  if (status === 'loading') {
    return <p>Loading organization data...</p>
  }

  // all good! display organization data
  return <OrganizationDetail organization={organization} />;
}

Using document writes in Firestore

Unlike queries, Reactfire does not provide any hook to write to documents.

But we can easily make our own by using useState (NB: the following is a a simplified example, but you'll get the gist).

import { useCallback, useState } from "react";

import {
  collection,
  addDoc,
} from 'firebase/firestore';

export function useCreateOrganization() {
  const firestore = useFirestore();
  const [state, setState] = useState();

  const mutation = useCallback(async (organization: Organization) => {
    setState('loading');

    const organizationsPath = `/organizations`;
    const collectionReference =
      collection(firestore, organizationsPath);

    try {
      await addDoc(
        collectionReference,
        organization
      );

      setState('success');
    } catch (e) {
      setState('error');
    }
  }, [firestore]);

  return [
    mutation,
    state,
  ] as [
    typeof mutation,
    typeof state,
  ];
}

Using the pattern above, we send the mutation as the first element of the array, and the state of the mutation as the second element.

The pattern can be used as follows:

function OrganizationForm() {
  const [
    createOrganization,
    createOrganizationState
  ] = useCreateOrganization();

  if (createOrganizationState === 'error') {
    return <p>Ops, we couldn't create this organization</p>;
  }

  if (createOrganizationState === 'loading') {
    return <p>Creating your organization. Please Wait....</p>;
  }

  if (createOrganizationState === 'success') {
    return <p>Yay! It worked.</p>;
  }

  return (
    <OrganizationForm onSubmit={createOrganization} />
  );
}

You can also use the same patterns for deleting or updating a document.

Should you use Firestore with your Next.js app?

OK, we've seen that Firestore is solid. But is it the best choice for your application? Let's talk about this.

Many developers start using Firestore, only to meet certain obstacles along the way: rigidity of the data model, need for full-text search, highly relational data structures, and so on.

The above are all signs that Firestore may not have been the best choice for your application or you as a developer (as you may prefer relational databases, for example).

Below is my non-exhaustive checklist for choosing (or not) Firestore:

  • I don't need a full-text search, OR I am ready to integrate and duplicate data using a dedicated search engine (Algolia, ElasticSearch, MeilliSearch, TypeSense, etc.)
  • I am OK with duplicating data, and I know how and where to do so
  • I don't need to make a large number of reads, or I am able to load documents progressively (for example, using pagination)
  • I need/want real-time updates
  • I need/want offline persistence

Final Words

Firestore is a robust database: easy to start with, secure, and scale. But, at the same time, it's not always the correct choice, so it's worth remembering if you're embarking on starting a SaaS using it.

For example, MakerKit uses Firestore extensively: while it's OK for most applications, it won't be the perfect choice for several use-cases, and that's fine.

If you need any help or want to know more about how we use Firestore in MakerKit, please reach out and let's talk.


Stay informed with our latest resources for building a SaaS

Subscribe to our newsletter to receive updatesor

Read more about

Cover Image for How to sell code with Gumroad and Github

How to sell code with Gumroad and Github

·7 min read
Sell and monetize your code by giving private access to your Github repositories using Gumroad
Cover Image for Migrating to Next.js Server Components Layouts

Migrating to Next.js Server Components Layouts

·6 min read
A simple guide to migrating your _app.tsx component to the new Server Components released with Next.js 13
Cover Image for Getting Started with Next.js Server Components

Getting Started with Next.js Server Components

·8 min read
A simple introduction to using Server Components and the new Layouts Folder Structure with Next.js 13
Cover Image for Counting a collection's documents with Firebase Firestore

Counting a collection's documents with Firebase Firestore

·2 min read
In this article, we learn how to count the number of documents in a Firestore collection using a custom React.js hook.
Cover Image for Pagination with React.js and Firebase Firestore

Pagination with React.js and Firebase Firestore

·6 min read
In this article, we learn how to paginate data fetched from Firebase Firestore with React.js
Cover Image for Building Multi-Step forms with React.js

Building Multi-Step forms with React.js

·12 min read
In this article, we explain how to build Multi-Step forms with Next.js and the library react-hook-form