angular / angularfire

Angular + Firebase = ❤️
https://firebaseopensource.com/projects/angular/angularfire2
MIT License
7.68k stars 2.19k forks source link

AngularFire v18 ng serve error when using zoneless change detection: Zone is not defined #3537

Open anisabboud opened 4 months ago

anisabboud commented 4 months ago

Version info

Angular: 18.0.0 AngularFire: 18.0.0 Firebase: 10.12.1

How to reproduce these conditions

Follow the Angular 18 guide to enable zoneless:

  1. Add provideExperimentalZonelessChangeDetection() to providers.
  2. remove zone.js from polyfills in angular.json.
  3. Run ng serve

https://blog.angular.dev/angular-v18-is-now-available-e79d5ac0affe

Debug output

Error in the terminal Zone is not defined

/myproject/node_modules/@angular/fire/fesm2022/angular-fire.mjs:101
    this.outsideAngular = ngZone.runOutsideAngular(() => new ɵZoneScheduler(Zone.current));
                                                                            ^

ReferenceError: Zone is not defined
    at eval (/myproject/node_modules/@angular/fire/fesm2022/angular-fire.mjs:101:77)
    at input (/myproject/node_modules/@angular/core/fesm2022/core.mjs:15827:16)
    at _ɵAngularFireSchedulers (/myproject/node_modules/@angular/fire/fesm2022/angular-fire.mjs:101:34)
    at Object._ɵAngularFireSchedulers_Factory (/myproject/node_modules/@angular/fire/fesm2022/angular-fire.mjs:106:12)
    at eval (/myproject/node_modules/@angular/core/fesm2022/core.mjs:3233:47)
    at runInInjectorProfilerContext (/myproject/node_modules/@angular/core/fesm2022/core.mjs:871:9)
    at R3Injector.hydrate (/myproject/node_modules/@angular/core/fesm2022/core.mjs:3232:21)
    at R3Injector.get (/myproject/node_modules/@angular/core/fesm2022/core.mjs:3095:33)
    at injectInjectorOnly (/myproject/node_modules/@angular/core/fesm2022/core.mjs:1106:40)
    at ɵɵinject (/myproject/node_modules/@angular/core/fesm2022/core.mjs:1112:42)

Node.js v20.13.1

Expected behavior

AngularFire/ng serve should work with the new zoneless change detection introduced in Angular v18. https://angular.dev/guide/experimental/zoneless

Actual behavior

ng serve crashes when opening localhost:4200.

spock123 commented 4 months ago

@anisabboud it's because @angular/fire code expects zoneJS to be present. What you can do is this workaround (although not perfect solution): keep zoneJS in your polyfills, but still use zoneless change detection. This will work, but of course zoneJS will still be part of your initial payload. At least you won't be using it. It would be nice if the code using zoneJS could check for existence of it before blindly using it. That would solve thing I guess.

But to be clear: we are running zoneless without issues - the only thing is that we have to keep zoneJS in polyfills, but that's it. It's still zoneless, but of course the initial payload is a tiny bit larger than it should have been. But this is a small price to pay to move to zoneless, we'll get to remove zonejs eventually I am sure.

jamesdaniels commented 4 months ago

We're working to support Zoneless Angular in an upcoming release

spock123 commented 4 months ago

@jamesdaniels did I say you rock? you do

duxor commented 4 months ago

The same topic is here: https://github.com/angular/angularfire/issues/3494

kiakahaDZ commented 2 months ago

any solution found plz i have the same issue

Benzilla commented 2 months ago

Eagerly waiting on this - AngularFire is the only package currently stopping us from going zoneless

wis-dev commented 1 month ago

@jamesdaniels any update about this? Maybe some release date. I'm exiting to go Zoneless with Firebase 🔥

Benzilla commented 1 month ago

Slightly off-topic, but if you're trying to go Zoneless, you're probably doing it for performance gains.

I went through the process of converting my entire SSR Angular 18 app to use signals for zoneless change detection and was waiting on AngularFire to support zoneless so I could finally remove zone.js from polyfills to "officially" go completely zoneless.

In the process I realised that I no longer actually used any of the benefits of AngularFire, and decided to move over to the official Firebase package. This let my project go zoneless, and also reduces bundle size further and removes dependencies.

It's not something I'd considered because everytime I've built with Angular & Firebase, AngularFire was the first thing I reached for, but it seems maybe with these latest Angular developments it's not as needed? Either way, I've loved using this package and built some awesome stuff with it - thanks for all the support and work on it.

anisabboud commented 1 month ago

@Benzilla can you please elaborate on the steps this migration entails?

For example, what's the replacement for the AngularFire initialization in app.config.ts?

export const appConfig: ApplicationConfig = {
  providers: [
    provideFirebaseApp(() => initializeApp({ ... })),
    provideFirestore(() => getFirestore()),
    provideAuth(() => getAuth()),
    provideFunctions(() => getFunctions()),
    provideStorage(() => getStorage()),
    provideAppCheck(() => ...),
    ...
  ],
  ...
})
spock123 commented 1 month ago

@Benzilla agree, I am looking to do the same, I can't wait any longer now... it would be cool with a minimal example setup that can be used as a starting point - I scoured the Internet but didn't find a single example , unfortunately.

muhamedkarajic commented 1 month ago

Can someone help me I have following error:

Error: NG0908: In this configuration Angular requires Zone.js

import { enableProdMode, isDevMode, provideExperimentalZonelessChangeDetection } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { AppModule } from './app/app.module';

const providers: any[] = [
  provideExperimentalZonelessChangeDetection(),
];

if (!isDevMode()) {
  enableProdMode();
}

platformBrowserDynamic(providers)
  .bootstrapModule(AppModule)
  .catch(err => console.error(err));

When I keep zone.js in polyfills then it works. I also have @angular/localize/init in polyfills.

Any help? Maybe @spock123?

Benzilla commented 1 month ago

@Benzilla can you please elaborate on the steps this migration entails?

For example, what's the replacement for the AngularFire initialization in app.config.ts?

export const appConfig: ApplicationConfig = {
  providers: [
    provideFirebaseApp(() => initializeApp({ ... })),
    provideFirestore(() => getFirestore()),
    provideAuth(() => getAuth()),
    provideFunctions(() => getFunctions()),
    provideStorage(() => getStorage()),
    provideAppCheck(() => ...),
    ...
  ],
  ...
})

Despite AngularFire's docs, I don't think this is a good way to initialise Firebase or AngularFire anymore, and that they should be initialised in injectable services. Otherwise you are always including AngularFire / Firebase in your main bundle every single time which increases your initial chunk file transfer size and is bad for core web vitals.

Angular states this in their docs that this is the preferred way, and also states its good for tree shaking etc.

Anecdotally initialising them in services reduced my initial chunk file estimated transfer size from 209.78 kB down to 94.27 kB (over 50%!) and has shown a positive improvement to core web vitals. I think maybe the AngularFire docs shouldn't state initialising in ApplicationConfig is a good thing to do anymore?

In my project I have separate services:

In the constructor of each service I initialise firebase in a similar pattern, for example for Database:

import { Injectable } from '@angular/core';
import { initializeApp } from "firebase/app";
import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore';
import { environment } from '../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class DatabaseService {

  private firestore : any;

  constructor(){
    const app = initializeApp(environment.firebase);
    this.firestore = getFirestore(app);
    if (!environment.production) {
      if(!this.firestore._settingsFrozen){ //Prevents initialising the emulator twice on SSR.
        connectFirestoreEmulator(this.firestore, 'localhost', 8080);
      }
    }
  }
}

For Auth

import { Injectable } from '@angular/core';
import { getAuth, connectAuthEmulator } from 'firebase/auth';
import { initializeApp } from "firebase/app";
import { environment } from '../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class AuthProviderService {

  public auth: any;

  constructor(){
    const app = initializeApp(environment.firebase);
    this.auth = getAuth(app);
    if (!environment.production) {
      connectAuthEmulator(this.auth, 'http://localhost:9099', { disableWarnings: true })
    }
  }
}

I can then just import these services into any component and access the firebase objects, and Angular should optimise via the builder and tree shaking.

I struggled like @spock123 to find examples for Angular + official Firebase package online, as if you Google Angular + Firebase this package dominates the SERPs, so I'm not sure if this is the best way to do things - but I've been running it in a production app for a week now with no problems and improved performance overall.

LanderBeeuwsaert commented 1 month ago

@Benzilla ooooh that's a really interesting approach for lazy loading the packages. We have similar services, as well as some landing & manual pages that don't need any firebase functionality. So your solution is great for that! Too bad indeed there is no guidance/documentation from angularFire about this.

jits commented 1 month ago

Give the conversations about using Firebase directly, without AngularFire, I hope folks don't mind me promoting the approach I've taken for the FullStacksDev Angular and Firebase tech stack: https://github.com/FullStacksDev/angular-and-firebase-template

You can get a quick overview of how I've set this up: https://github.com/FullStacksDev/angular-and-firebase-template/blob/main/ARCHITECTURE.md#app-accessing-firebase-services-from-the-angular-app. I then use rxfire (which AngularFire uses under the hood) directly in my services (to fit nicely with RxJS). Happy to take more questions etc. in the repo I linked to, if folks are interested.

IsmaelBembo commented 1 month ago

@Benzilla

In the constructor of each service I initialise firebase in a similar pattern, for example for Database:

I've been trying to do this for a long time before encountering your comment, which it looks like it should work but I can't get rid of this error:

image

This is my current state:

image

image

Any ideas?

ciriousjoker commented 1 month ago

@IsmaelBembo You need to switch from AngularFire to the regular firebase package. @Benzilla Your approach doesn't seem to work well with SSR. For some reason, during SSR, the getDocs (replacement for collectionData) freezes completely. Had to switch back to AngularFire for that reason.

In general though: Maybe don't completely hijack this issue.

dalenguyen commented 1 month ago

@IsmaelBembo You need to switch from AngularFire to the regular firebase package. @Benzilla Your approach doesn't seem to work well with SSR. For some reason, during SSR, the getDocs (replacement for collectionData) freezes completely. Had to switch back to AngularFire for that reason.

In general though: Maybe don't completely hijack this issue.

I think you need to wrap it inside afterNextRender for it to work with SSR.

Benzilla commented 1 week ago

@Benzilla Your approach doesn't seem to work well with SSR. For some reason, during SSR, the getDocs (replacement for collectionData) freezes completely. Had to switch back to AngularFire for that reason.

@ciriousjoker Should work fine with SSR (my app is fully SSR).

You need to make sure your app is zoneless, and is using ExperimentalPendingTasks for SSR: https://angular.dev/api/core/ExperimentalPendingTasks

riya-amemiya commented 1 week ago

Is there any progress on this? I'm looking forward to Angular Fire from Zoneless!