15

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?.

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
Mostafa Hesham
  • 547
  • 4
  • 14
  • Do you enable Persistence parameter from your database settings? I found [this GitHub issue](https://github.com/invertase/react-native-firebase/issues/2953) where Persistence is being mentioned with a similar error. – Artemis Georgakopoulou Dec 02 '20 at 13:42
  • @ArtemisGeorgakopoulou no, I actually didn't add any custom settings I only using the `useEmulator` method but before that, I add the local host on development as this ```js firestore().settings({ host: 'http://localhost:8080', ssl: false }) ``` but I had this error ``` 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. ``` very often when I used the useEmulator it happens but less often. – Mostafa Hesham Dec 02 '20 at 20:01
  • can anyone above 1500 please suggest the Firebase-emulators tag to be used on StackOverflow? – Mostafa Hesham Dec 02 '20 at 20:04
  • In the line " db.useEmulator('localhost', 8888); ", have you tried setting the port 8080 instead as configured in the settings? – Artemis Georgakopoulou Dec 03 '20 at 13:42
  • well 8080 is used by Postgres so I configured 8888 as the port for firestore – Mostafa Hesham Dec 04 '20 at 14:47

4 Answers4

13

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 .

Mostafa Hesham
  • 547
  • 4
  • 14
  • 1
    I got a typescript error that stopped this from working for some reason but changed file to .js and now works. Thanks. – leto Oct 26 '21 at 13:44
6

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.

Reactgular
  • 52,335
  • 19
  • 158
  • 208
  • 1
    I would like to mark it as the solution but I think I will give it a day or more to ensure it was the solution because the bug I face isn't always happening. thanks – Mostafa Hesham Mar 11 '21 at 23:02
  • 1
    Can you explain why you `db.useEmulator()` when the code is being run on the server (`typeof window === 'undefined'`)? I would think that we'd only want to initialize the emulator with client-side code? So far it appears that just connecting the firestore emulator client-side works well (if `typeof window !== 'undefined'`)... unless I'm missing something? – Jason Frank Feb 26 '22 at 18:07
3

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()
  }
}
Spain Train
  • 5,890
  • 2
  • 23
  • 29
0

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);
}
João Melo
  • 784
  • 6
  • 16
  • what are the config values? – Sarah K Mar 03 '22 at 00:32
  • 1
    the config values are (1) suite which hold a instance of the firebase app and the firebase projectId and (2) the emulator host which a keep in a config file but really never changed from "8080". – João Melo Mar 04 '22 at 10:50