January 27, 2019

Firebase and Gatsby, Together At Last

or How I Fixed an Unexpected Snafu
edit

If you look just above the title of this post, you should see a beard icon and the phrase ${strokes} bestowed. It’s a fun little indicator of how many likes this post has received to date. I built this using a Firebase Realtime Database. For those of you unfamiliar with Firebase, it’s a cool, JSON-based DBaaS (database-as-a-service) product from Google that I’ve used in a few other projects.

Firebase comes with a JavaScript SDK that’s normally a cinch to hook up to a client side code. In fact, I had zero complications with combining Gatsby and Firebase until I tried to deploy my new feature. That’s when things started hitting the fan.

A Missing Window

The first issue I ran into was when I tried to run a build. Gatsby ran into some issues with Firebase. I was really confused at first because Firebase was working just fine in Gatsby’s development environment. I wasn’t expecting it to crash and burn so badly when it was built.

Turns out that Firebase’s initialization code makes a reference to the window object. This isn’t a problem in development, because we’re using Webpack’s dev server. But running gatsby build is a bit different. window does not exist in the build environment and you have to be careful where and when you try to access the object. Firebase was crashing every build because it was trying to access a property on this non-existent object.

Ok, how do we solve this?

After going down a few wrong paths (and reading some Github issues), I landed on a solution I’m pretty happy with. I ended up changing how, more precisely when, Firebase was initialized in the application. At first, I followed a tried and true method for initializing the app that looks like this:

import firebase from 'firebase/app'
import 'firebase/database'

const config = {
  apiKey: process.env.FIREBASE_API_KEY,
  authDomain: process.env.FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.FIREBASE_DATABASE_URL,
  projectId: process.env.FIREBASE_PROJECT_ID,
  storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
}

firebase.initializeApp(config)

export default firebase

export const database = firebase.database()

With this strategy, I have a file, firebase.js, that initializes the app right away and exports the initialized firebase instance around the application. More importantly, it exports my database around the application to be used in whatever components need to hook up to it.

The problem with this strategy, as mentioned before, is that the window object is not available to the initializeApp method during the build process (not to mention, those environment variables aren’t correct either, but I’ll address that later in the post).

To solve this, I need Firebase to delay initializing until we’re in the client side environment. But, I still want to have a single instance in the app and a way to export the database. This calls for a refactor that makes use of dynamic imports and a singleton pattern.

Let’s make it happen.

Simple Functions to the Rescue

Often, I often find a simple function ends up being the best solution to a problem. First, I started by changing the code in firebase.js to look like this:

const config = {
  apiKey: process.env.FIREBASE_API_KEY,
  authDomain: process.env.FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.FIREBASE_DATABASE_URL,
  projectId: process.env.FIREBASE_PROJECT_ID,
  storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
}

let firebaseInstance
export const getFirebase = firebase => {
  if (firebaseInstance) {
    return firebaseInstance
  }

  firebase.initializeApp(config)
  firebaseInstance = firebase

  return firebase
}

We don’t import any of the firebase packages into the module. Instead, we export a function that receives the firebase package as an argument. Inside this function, we check that an instance of firebase does not previously exist (held in closure by the module). If it does exist, return the initialized instance, otherwise proceed with initializing firebase and return it. This is the singleton pattern I mentioned before.

Now, in the components that need firebase, I can wait until the componentDidMount lifecycle method has been called, which guarantees that the window object exists. Once I can make this guarantee, I can then dynamically import the firebase modules I need and pass them into my function. The firebase instance is returned from the function, which I can then get the database from. That code looks like this:

componentDidMount() {
  const lazyApp = import('firebase/app')
  const lazyDatabase = import('firebase/database')

  Promise.all([lazyApp, lazyDatabase]).then(([firebase]) => {
    const database = getFirebase(firebase).database()
    // do something with `database` here,
    // or store it as an instance variable or in state
    // to do stuff with it later
  })
}

// I might update this with hooks once they're released officially.
// You'll have to come back and find out.

With this code in place, I can make reads and writes to the Firebase database from a component. I thought this would be the end of my problems. I was wrong.

Gatsby, Netlify, and Environment Variables

Now, before I go into this I want to make it clear, I read the documentation for all of these things: Gatsby’s environment variables, Netlify’s environment variables. I thought I had it all set up correctly. I did not. I ended up reading (and reading) the docs a few more times before I finally understood what I needed to do.

For those of you who might not be familiar with environment variables, let me give you a brief explanation. As the name suggests, these are variables that are exposed to a particular environment, such as development, test, or production. For example, you might want to have an API_ENDPOINT variable in your code that is different depending on whether you’re in development or production. Thus, you inject these different values into the environment when it is instantiated, and they’re made available to your code via the process.env object.

Environment variables are a great way to keep private code private (yes, I know it’s not perfect, but bear with me. You can read up about the challenges of environment variables elsewhere). Gatsby and Netlify both have a way to inject these variables into your build. But there’s a bit of a catch with Gatsby.

Gatsby makes a distinction between two kinds of environment variables. They call them “Project env vars” and “OS env vars”. In my original implementation, I did not prefix my env vars with GATSBY_, thinking I understood how these variables worked. My code was working locally, my environment variables were correctly injected and Firebase was initializing, but I soon realized that none of my variables were getting injected properly in the production build.

I was baffled, I had also set up my variables correctly with Netlify. I double and triple checked them. Then, I read the docs again and realized that Gatsby will only make available “OS env vars” to Netlify’s build. Thus I needed to prefix all my env vars in my project, in my local .env.* files, and on Netlify with GATSBY_. As soon as I did that, the build passed and my blog was back to working.

// adding `GATSBY_` made it all better
const config = {
  apiKey: process.env.GATSBY_FIREBASE_API_KEY,
  authDomain: process.env.GATSBY_FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.GATSBY_FIREBASE_DATABASE_URL,
  projectId: process.env.GATSBY_FIREBASE_PROJECT_ID,
  storageBucket: process.env.GATSBY_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.GATSBY_FIREBASE_MESSAGING_SENDER_ID,
}

Conclusion

Gatsby and Firebase will work great together, but you’ll have to ensure that Firebase isn’t initialized until the window object is available. And double check your environment variables setup if you’re struggling to get them defined during Netlify’s build of your app.

Best of luck if you run into similar problems. Let me know what you think of my solution on Twitter!


Liked the post?
Give the author a dopamine boost with a few "beard strokes". Click the beard up to 50 times to show your appreciation.
Kyle Shevlin's face, which is mostly a beard with eyes

Kyle Shevlin is the founder & lead software engineer of Agathist, a software development firm with a mission to build good software with good people.

Logo for Introduction to State Machines and XState
Introduction to State Machines and XState
Check out my courses!
If you enjoy my posts, you might enjoy my courses, too. Click the button to view the course or go to Courses for more information.
Sign up for my newsletter
Let's chat some more about TypeScript, React, and frontend web development. Unsubscribe at any time.