Using the Firebase Local Emulator with Expo's managed workflow

While the Local Emulator Suite is super easy to set up on a web app project, it's a little bit trickier for React Native / Expo.

Firebase is Google's mobile platform that helps you quickly develop high-quality apps and grow your business. They acquired it in 2014 and now use it as their flagship offering for app development. It's essentially a high-level UI and SDK that sits on top of Google Cloud services, making it super easy for developers to spin up apps quickly with managed services such as authentication, databases, storage and cloud functions.

When you're developing a Firebase app, it's important not to mix your testing data with your live data. To manage this, Firebase have released their Local Emulator Suite - a set of tools designed to run a (mostly) fully-fledged Firebase setup on your local machine.

While the Local Emulator Suite is super easy to set up on a web app project, it's a little bit trickier for React Native / Expo. The standard React Native x Firebase library (aptly named react-native-firebase) requires you to eject from the Expo managed workflow, so we'll need to use the Web SDK.

To start, let's install all the dependencies we'll need for this setup:

  • Firebase, obviously: expo install firebase
  • Firebase Core: expo install expo-firebase-core
  • Expo Constants: expo install expo-constants

For this post, I'm going to assume you've gone and set up a Firebase project on their site. You'll need a Web project as we've mentioned above. You'll be given a big JSON object of config data, so let's copy and paste that into your web.config.firebase property in your config file (app.json or app.config.ts):

{
  "web": {
    "config": {
      "firebase": {
        "appId": "1:1082251606918:ios:a2800bc715889446e24a07",
        "apiKey": "AIzaXXXXXXXX-xxxxxxxxxxxxxxxxxxx",
        "clientId": "000000000000-0000000000000.apps.googleusercontent.com",
        "trackingId": 12345567890,
        "databaseURL": "https://myexpoapp777.firebaseio.com",
        "storageBucket": "myexpoapp777.appspot.com",
        "projectId": "myexpoapp777",
        "messagingSenderId": 123454321
      }
    }
  }
}

Now, let's connect to our Firebase instance:

import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';
import 'firebase/functions';
import 'firebase/storage';
 
function initialiseFirebase() {
  const config = FirebaseCore.DEFAULT_WEB_APP_OPTIONS;
 
  if (!firebase.apps.length) {
    firebase.initializeApp(config as FirebaseCore.IFirebaseOptions);
  }
}
 
initialiseFirebase();

Awesome, now we have our Firebase app up and running! The next step is to check whether we're running our app in a development environment and switch to the Local Emulator Suite accordingly. Let's do that by using __DEV__, an environment variable that holds our environment:

import * as FirebaseCore from 'expo-firebase-core';
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';
import 'firebase/functions';
import 'firebase/storage';
 
function initialiseFirebase() {
  const config = FirebaseCore.DEFAULT_WEB_APP_OPTIONS;
 
  if (!firebase.apps.length) {
    firebase.initializeApp(config as FirebaseCore.IFirebaseOptions);
    if (__DEV__) {
      console.log('Switching to local Firebase instance...');
      const origin = 'localhost';
 
      firebase.auth().useEmulator(`http://${origin}:9099/`);
      firebase.firestore().useEmulator(origin, 8080);
      firebase.functions().useEmulator(origin, 5001);
    }
  }
}
 
initialiseFirebase();

Great! If we open the iOS Simulator, we should now be able to connect to our Local Emulator Suite.

Trouble is, the origin localhost only works with a simulator. This is because the IP address of the simulated phone is the same as the Firebase instance. If you try this with a physical device, it won't work. It's trying to connect to the localhost Firebase instance on the physical phone, which doesn't exist. You'll probably end up with an error similar to this one:

"@firebase/firestore, Firestore (7.8.1): Could not reach Cloud Firestore backend. Connection failed 1 times. Most recent error: FirebaseError: [code=unavailable]: The operation could not be completed This typically indicates that your device does not have a healthy Internet connection at the moment. The client will operate in offline mode until it is able to successfully connect to the backend."

To fix this, we can check the Debugger Host (the IP address of the running Metro Bundler instance) which is stored in Expo Constants:

import Constants from 'expo-constants';
import * as FirebaseCore from 'expo-firebase-core';
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';
import 'firebase/functions';
import 'firebase/storage';
 
function initialiseFirebase() {
  const config = FirebaseCore.DEFAULT_WEB_APP_OPTIONS;
 
  if (!firebase.apps.length) {
    firebase.initializeApp(config as FirebaseCore.IFirebaseOptions);
    if (__DEV__) {
      console.log('Switching to local Firebase instance...');
      const origin =
        Constants.manifest.debuggerHost?.split(':').shift() || 'localhost';
 
      firebase.auth().useEmulator(`http://${origin}:9099/`);
      firebase.firestore().useEmulator(origin, 8080);
      firebase.functions().useEmulator(origin, 5001);
    }
  }
}
 
initialiseFirebase();

We'll also need to update our firebase.json file to set the various emulator hosts to 0.0.0.0. This tells your computer to use localhost and your Local IP for your network at the same time.

"emulators": {
  "functions": {
    "port": 5001,
    "host": "0.0.0.0"
  },
  "firestore": {
    "port": 8080,
    "host": "0.0.0.0"
  },
  "auth": {
    "port": 9099,
    "host": "0.0.0.0"
  }
}

Let me know if you have any questions or comments! I'm always happy to help out.


Published on May 18, 2021 4 min read