Mostafa Hesham
Mostafa Hesham

Reputation: 547

Firebase Firestore emulator error `Host has been set in both settings() and useEmulator(), emulator host will be used`

first of all that is the full error I got.

@firebase/firestore: Firestore (8.1.1): Host has been set in both settings() and useEmulator(), emulator host will be used
Error [FirebaseError]: Firestore has already been started and its settings can no longer be changed. You can only modify settings before calling any other methods on a Firestore object.

this is how I init the emulator

const db = app.firestore();
const auth = firebase.auth();
if (process.env.NODE_ENV === 'development') {
  db.useEmulator('localhost', 8888);
  firebase.auth().useEmulator('http://localhost:9099/');
}

the project is running nextjs when I first start the application everything run as expected but after some refreshing or navigation among next.js pages, I suddenly get this error. and I have to kill the terminal and start over which is annoying I don't know if next.js server runs the if (process.env.NODE_ENV === 'development') code several times and this could be the cause of this error if that is case how to avoid setting a new emulator when there is one already. or is it a bug related to firebase emulators?.

Upvotes: 15

Views: 5314

Answers (4)

Spain Train
Spain Train

Reputation: 6006

Unfortunately, the accepted answer did not work for me. I ended up needing to use getApp even though I was memoizing my init function. Here is the code that finally fixed this issue for me (same context: Next.js app with hot reload):

const firebaseConfig = {...}

const initFirebase = once(() => {
  console.log('Initializing firebase')

  const app = initializeApp(firebaseConfig)
  const auth = getAuth(app)
  const functions = getFunctions(app)
  const db = getFirestore(app)

  if (process.env.NEXT_PUBLIC_FIREBASE_USE_EMULATOR === 'true') {
    console.log('Attching firebase emulators')

    connectFirestoreEmulator(db, 'localhost', 8080)
    connectAuthEmulator(auth, 'http://localhost:9299')
    connectFunctionsEmulator(functions, 'localhost', 5001)
  }
  return { auth, functions, db }
})

export const getFirebase = () => {
  try {
    const app = getApp()
    const auth = getAuth(app)
    const functions = getFunctions(app)
    const db = getFirestore(app)
    return { auth, functions, db }
  } catch (e) {
    return initFirebase()
  }
}

Upvotes: 3

Mostafa Hesham
Mostafa Hesham

Reputation: 547

after trying almost all of the solutions here it didn't really work the bug was happening from time to time the annoying thing is that I didn't know how to reproduce it but I think it happens when this page has a server-side error anyway the solution I used for getting around this bug was the following

const EMULATORS_STARTED = 'EMULATORS_STARTED';
function startEmulators() {
  if (!global[EMULATORS_STARTED]) {
    global[EMULATORS_STARTED] = true;
    firebase.firestore().useEmulator('localhost', 8888);
    firebase.auth().useEmulator('http://localhost:9099/');
  }
}

if (process.env.NODE_ENV === 'development') {
  startEmulators();
}

but for this to work like expected you will need to make sure that all emulators have started before making a request to the next.js server because if this code was executed before the emulators start then global[EMULATORS_STARTED] would be true and it will never use the emulators in this case. I have tested this on so many pages and some of them had server-side errors and the bug wasn't happening instead I got logs of these errors which is the expected behavior I didn't know these Errors existed in the first place before applying this solution 😁.

Upvotes: 13

João Melo
João Melo

Reputation: 835

I agree with @Reactgular assessment about the cause. But in my case, the global solution is not well suited because the error is thrown during tests, and they should be isolated.

I also had to deal with a "clear emulator data" logic, that opposite to the useEmulator should be called at every initialization.

To solve that, I found that an inner "host" property inside the Firestore object changes after we call useEmulator. I then started to check that property before commanding the useEmulator.

Here is the code:

import { del } from '../request';

async function initFirestore (config) {
  const { suite, firestoreEmulatorHost } = config;
  const { app, projectId } = suite;

  const firestore = app.firestore();

  if (firestoreEmulatorHost) {
    plugEmulator(firestore, firestoreEmulatorHost);
    await clearFirestoreEmulator(projectId, firestoreEmulatorHost);
  }

  return firestore;
}

export function plugEmulator (firestore, firestoreEmulatorHost) {
  const settingsHost = firestore._delegate._settings.host;
  const isUnplugged = !settingsHost.includes(firestoreEmulatorHost);

  if (isUnplugged) {
    firestore.useEmulator('localhost', firestoreEmulatorHost);
  }
}

export function clearFirestoreEmulator (projectId, firestoreEmulatorHost) {
  const clearUrl = `http://localhost:${firestoreEmulatorHost}/emulator/v1/projects/${projectId}/databases/(default)/documents`;
  return del(clearUrl);
}

Upvotes: 0

Reactgular
Reactgular

Reputation: 54811

NextJs is hot-reloading the web page, and xxx.useEmulator(...) is being called twice for the same browser instance.

Under the hood the Firebase library uses a global reference to the current app, and from the perspective of the library you're trying to initialize it twice or more.

You can reproduce this problem with the following code:

const db = app.firestore();
db.useEmulator('localhost', 8888);
db.useEmulator('localhost', 8888); // raises error

The only work-around that I've found is to use the window object to hold a flag if it's been initialized or not, but you also have to handle the edge case of SSR.

const db = app.firestore();
if(typeof window === 'undefined' || !window['_init']) {
   db.useEmulator('localhost', 8888);
   if(typeof window !== 'undefined') {
      window['_init'] = true;
   }
}

It's not the most elegant code above but it fixes the error.

The key is to know that hot reloading is the problem, and Firebase should only be configured once.

Upvotes: 6

Related Questions