Reputation: 547
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
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
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
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
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