angular/angularfire

Inject() must be called from an injection context such as a constructor in AngularFirestoreDoc after upgrading to Angular v19

djamn opened this issue · 23 comments

I recently upgraded all dependencies to Angular v19 (from v18). I was previously on Angular v18 where everything worked fine. However, now, my whole console is spammed with the following error:

ERROR RuntimeError: NG0203: inject() must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`. Find more at https://angular.dev/errors/NG0203
    at injectInjectorOnly (core.mjs:1104:11)
    at ɵɵinject (core.mjs:1114:40)
    at inject (core.mjs:1199:10)
    at <instance_members_initializer> (angular-fire-compat-firestore.mjs:93:14)
    at new AngularFirestoreDocument (angular-fire-compat-firestore.mjs:98:3)
    at _AngularFirestore.doc (angular-fire-compat-firestore.mjs:621:12)
    at auth.service.ts:38:14
    at switchMap.js:16:17
    at OperatorSubscriber2._this._next (OperatorSubscriber.js:14:9)
    at Subscriber2.next (Subscriber.js:32:12)

However, my auth service at line 38:14 only has the following code:
.doc<User>(`users/${user.uid}`)

The whole constructor looks like this (it is a angular service class)

  providedIn: 'root',
})
export class AuthService {
  isLoggedIn$: Observable<boolean>;
  /** User data of database */
  user$: Observable<User | null | undefined>;
  userRoles$: Observable<string[]>;
  userId: string | undefined = undefined;
  username: string | undefined = undefined;

  constructor(
    readonly firestore: AngularFirestore,
    readonly fireAuth: AngularFireAuth,
    readonly router: Router,
  ) {
    this.user$ = this.fireAuth.authState.pipe(
      switchMap((user) => {
        if (user) {
          this.userId = user.uid;
          return this.firestore
            .doc<User>(`users/${user.uid}`)
            .valueChanges()
            .pipe(
              map((userData) => {
                if (userData) {
                  this.username = userData.username; // Assign username
                }
                return userData;
              }),
            );
        } else {
          this.userId = undefined;
          this.username = undefined;
          return new Observable<User | null>((observer) => observer.next(null));
        }
      }),
    );

But I dont really know, why it does not work anymore in angular 19.

My package dependencies:

 "dependencies": {
    "@angular/animations": "^19.1.5",
    "@angular/cdk": "^19.1.3",
    "@angular/common": "^19.1.5",
    "@angular/compiler": "^19.1.5",
    "@angular/core": "^19.1.5",
    "@angular/fire": "^19.0.0",
    "@angular/forms": "^19.1.5",
    "@angular/material": "^19.1.3",
    "@angular/platform-browser": "^19.1.5",
    "@angular/platform-browser-dynamic": "^19.1.5",
    "@angular/router": "^19.1.5",
    "@fortawesome/angular-fontawesome": "^1.0.0",
    "@fortawesome/free-brands-svg-icons": "^6.7.1",
    "@fortawesome/free-regular-svg-icons": "^6.7.1",
    "@fortawesome/free-solid-svg-icons": "^6.7.1",
    "@ng-select/ng-select": "^14.2.2",
    "@ngx-translate/core": "^15.0.0",
    "@ngx-translate/http-loader": "^8.0.0",
    "angular-build-info": "^2.0.1",
    "canvas-confetti": "^1.9.3",
    "flowbite": "^2.5.2",
    "ng-recaptcha-2": "^15.0.2",
    "ngx-editor": "^18.0.0",
    "ngx-quill": "^27.0.0",
    "quill": "^2.0.3",
    "quill2-emoji": "^0.1.2",
    "rxjs": "~7.8.0",
    "tslib": "^2.8.1",
    "tw-elements": "^2.0.0",
    "zone.js": "^0.15.0"
  },

I also tried to set "preserveSymlinks": false, in angular.json however, the issue still persists.

This issue does not seem to follow the issue template. Make sure you provide all the required information.

Hi,

I have exactly the same issue.
I pass through by using following code await runInInjectionContext(this.injector, async () => { but I think it's a monkey patch that do not resolve the source.
Just want to know if there's a new way to implement or a real issue?

Thank you !

I am having the same issue with version 19.0.0, it worked with the rc.0 release candidate of version 19 before though.

I have confirmed that version 19.0.0-rc.0 is the latest version without this issue, is there some new way to implement this that is not documented or is this a real issue?

I believe that if you switch from the namespace syntax to the modular style that will solve the issue. https://firebase.google.com/docs/web/modular-upgrade

I believe that if you switch from the namespace syntax to the modular style that will solve the issue. https://firebase.google.com/docs/web/modular-upgrade

I hope there is still another solution, since my project is huge with multiple different services

You could wrap the modular methods in an Angular Service. Still a large change, but mostly just renaming things hopefully. I can describe more if interested.

The only other solution is to wrap every Firebase method in an injection context.

Or, stick with older versions.

You could wrap the modular methods in an Angular Service. Still a large change, but mostly just renaming things hopefully. I can describe more if interested.

Would appreciate it if you could provide more information!

Something like:

import {
  EnvironmentInjector,
  inject,
  Injectable,
  runInInjectionContext,
} from '@angular/core';
import { doc, Firestore } from '@angular/fire/firestore';
import type { DocumentReference } from '@angular/fire/firestore';

@Injectable({ providedIn: 'root' })
export class AngularFirestoreService {
  private readonly _firestore: Firestore = inject(Firestore);
  private readonly _injector: EnvironmentInjector = inject(EnvironmentInjector);

  /**
   * Note that the doc method could accept a CollectionReference or DocumentReference in addition to
   * Firestore.
   */
  public doc(path: string, ...pathSegments: string[]): DocumentReference {
    return runInInjectionContext(
      this._injector,
      (): DocumentReference => doc(this._firestore, path, ...pathSegments),
    );
  }
}

But note that I didn't test this code, it is off the cuff at 07:30 in the morning for me so it might be terrible.

So it works when migrating namespace syntax to modular syntax. In my case it would be the following:

  private auth: Auth;
  private firestore: Firestore;
  isLoggedIn$: Observable<boolean>;
  user$: Observable<User | null | undefined>;
  userRoles$: Observable<string[]>;
  userId: string | undefined = undefined;
  username: string | undefined = undefined;

  constructor(firestore: Firestore, private router: Router) {
    this.auth = getAuth();
    this.firestore = firestore;

    this.user$ = new Observable<User | null | undefined>((observer) => {
      onAuthStateChanged(this.auth, async (user) => {
        if (user) {
          this.userId = user.uid;
          const userDocRef = doc(this.firestore, `users/${user.uid}`);
          docData(userDocRef).subscribe((userData) => {
            if (userData) {
              this.username = (userData as User).username;
            }
            observer.next(userData as User | null);
          });
        } else {
          this.userId = undefined;
          this.username = undefined;
          observer.next(null);
        }
      });
    });

However, this is still a very breaking change to all services and I did not find anything in the changelog.