Luciano Serruya Aloisi

Developing locally with Firebase 🔥

In case you ever used any Firebase product in a project, you would quickly realize that you were always hitting your production project. Or maybe you were cautious enough to use a second Firebase project as a development environment. But what about developing locally? Can't we just run some commands in our machines to spin up a "Firebase server" so our app can communicate with it instead of reaching out to the cloud? As a matter of fact, you can do that! The Firebase Emulator has been around for quite some time now, but the Firebase team recently released the Firebase Emulator UI, so you can have an almost identical Firebase console locally.

Getting started

You need to have JDK 8+ installed to run the emulators

Although we will have a "local instance" of a Firebase server, we still need to have a Firebase project created, so we first need to do that before moving on. Go to the Firebase console and create a new project.

Then, we need to have the firebase CLI tool installed to initialize our project locally and run the emulators

npm install -g firebase-tools

To try out Firebase Emulators, we are going to create the simplest to-do app in React, so we first need to create a new project before continuing

npx create-react-app firebase-emulators-todo-app --template typescript && cd firebase-emulators-todo-app

We are going to use the Firebase Web SDK to directly store data from our React app, so we need to install it

npm install firebase

Once installed, we need to get our credentials to connect our app with the Firebase project. Go to the Firebase console, select your project, then click on the wheel right next to Project Overview, and select Project Settings. Scroll down until you see the Your apps section. Add a new web app, and copy your config object to your src/index.ts file

const firebaseConfig = {
  apiKey: "...",
  authDomain: "...",
  databaseURL: "...",
  projectId: "...",
  storageBucket: "...",
  messagingSenderId: "...",
  appId: "..."
};

We haven't initialized Firebase in our React app. To do so, run firebase init inside your React app directory, and using the space bar only select Emulators and then Firestore Emulators. It will ask you some questions about several configurations, which you can complete them by pressing Enter to use the default values. Once finished, start the emulators by running firebase emulators:start. If everything starts correctly, you can visit the Emulator UI by navigating to localhost:4000 (if using the default port)

Up to this point we could already start writing our app that will store data to our Firebase project hosted on the cloud. But that's not what we want, we want to communicate with our local emulator, so we need to tell Firebase somehow that we want to use our local instance (only in development)

// src/index.ts

import firebase from "firebase/app";
import "firebase/firestore";

firebase.initializeApp(firebaseConfig);

const env = process.env.NODE_ENV;
if (env.match(/development/i) || window.location.hostname === "localhost") {
  firebase.firestore().settings({
    host: "localhost:8080",
    ssl: false
  });
}

The setup would be exactly the same in case you were using Firestore as a database for a backend project using Firebase Admin, for example. You can see this repo if you are looking to use the Firebase Emulators with a express.js-based API

Application

We are good to go now. Our React app is set up to communicate with our local instance of Firebase. Open the Firebase Emulators UI (localhost:4000) and go to the Firestore Emulator. Create a new collection called todos and some documents. To keep it simple, we are creating to-dos with only a title property.

Go to your App.tsx file and add the following effect to retrieve our manually created to-dos

import React, { useEffect } from "react";
import firebase from "firebase/app";
import "firebase/firestore";

function App() {
  useEffect(() => {
    const fetch = async () => {
      const todos = (
        await firebase.firestore().collection("todos").get()
      ).docs.map(d => d.data());
      console.log({ todos });
    };
    fetch();
  }, []); 

  return (...)
}

export default App;

Start your React development server in case you weren't already running it with npm run start. Go to localhost:3000, open your console, and you should see your to-do created via the Firebase Emulator UI. Great!

In case you are getting an error when trying to fetch documents from Firestore, please check that you didn't initialize Firestore in your React app. In case you did, you will see a firestore.rules file. This file specifies security rules for your Firestore database. For this simple project, we won't need any, so you can just delete that file. If you want to keep it, you will have to update the only security rule it has (only allows authenticated requests to read and write data) to allow every request.

allow read, write: if true;

In order to add new todos, we will need a form. Our App component is the following

import React, { FormEvent, useEffect, useState } from "react";
import firebase from "firebase/app";
import "firebase/firestore";

interface Todo {
  id: string;
  title: string;
}

function App() {
  const [title, setTitle] = useState("");
  const [todos, setTodos] = useState<Todo[]>([]);

  useEffect(() => {
    const fetch = async () => {
      const todos = (
        await firebase.firestore().collection("todos").get()
      ).docs.map(doc => {
        return { id: doc.id, ...doc.data() } as Todo;
      });
      setTodos(todos);
    };
    fetch();
  });

  const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    try {
      await firebase.firestore().collection("todos").add({ title });
      alert("To-do created successfully!");
    } catch (e) {
      alert("Something went wrong :/");
    } finally {
      setTitle("");
    }
  };

  return (
    <div className="App">
      <section>
        <form onSubmit={handleSubmit}>
          <label htmlFor="title">Title:</label>
          <input
            type="text"
            name="title"
            id="title"
            value={title}
            onChange={e => setTitle(e.target.value)}
          />
        </form>
      </section>
      <section>
        <ul>
          {todos.map((todo, i) => (
            <li key={i}>{todo.title}</li>
          ))}
        </ul>
      </section>
    </div>
  );
}

export default App;

Add a couple of to-dos, and check your Firebase Emulators console. Go to the Firestore emulator tab and you should see them.

Persisting data between starts

Something that happens with the Firebase Emulators is that data isn't stored automatically when you stop the emulators and start them back again. This is easily achievable by passing two parameters when starting the emulators, --export-on-exit and --import <DIRECTORY>. So, the full command to start the emulators, store your data in a file, and import it next time you run the emulators is

firebase emulators:start --export-on-exit --import data

I called the directory Firebase Emulators will store data just data but you can use whatever name you want (remember to ignore it from your source control versioning)

Deployment

We can easily deploy our React SPA to Firebase Hosting. To do so, run once again firebase init in your React app directory, and this time only initialize Firebase Hosting (you can make it faster writing firebase init hosting). When asked what do you want to use as your public directory, input build, which is the directory React generates when building your app. Also write y when asked if you want to configure it as a single-page app.

Once done, build your app with npm run build, and deploy it with firebase deploy --only hosting

If you now navigate to the URL Firebase gives you after deploying your app, you may see in the console a permission error, which can be fixed by changing the security rules of your database in the Firebase console (the cloud version, not your local one). Deploy your security rules from the web editor and try again, it should work now.

Your deployed version of the app won't write data to your local emulator, but to your online Firestore database. To now check your data, go to the Firestore console in your Firebase project

Conclusion

In this short tutorial we saw how to set up a local Firebase Emulator, and how to connect a React data to it. We also saw how to deploy our app and use the online version of the database instead of the local one.

Hope you liked it!

🐦 @LucianoSerruya

📧 lucianoserruya (at) gmail (dot) com