Search code examples
angularangularfire2

Angularfire2. Cannot read property 'subscribe' of undefined


I'm trying to separate the logic from my components to a service, it works but still I'm getting console errors.

I'm constructing this service and consuming it in my component

@Injectable({
  providedIn: 'root'
})
export class DatabaseService {
  public businessDoc: AngularFirestoreDocument<Business>;
  public business: Observable<Business>;

  constructor(private auth: AngularFireAuth, private db: AngularFirestore) {
    this.auth.authState.subscribe(user => {
      this.uid = user.uid;
      this.businessDoc = this.db.doc<Business>(this.businessAddress);
      this.business = this.businessDoc.valueChanges()
    }
  }
  private get businessAddress() {
    return 'users/' + this.uid
  }
}

And this is my component

export class ConfigComponent implements OnInit {

  config: Config;

  constructor(public db: DatabaseService) {
    this.db.business.subscribe(res => {
      this.config = res.config;
    });
  }

Finally, in my template

<input [ngModel]="config?.regular" (ngModelChange)="onConfigChange($event)">

It compiles with no problem, as you can see it even renders properly in the view, but then in the browser console I get this:

Console error

If I initialize the the business observable in my service, like this public business: Observable<Business> = new Observable(), I don't get the error anymore, but then the component doesn't display anything

As I understand it, business does not yet exist in the service because it's either waiting for businessDoc to connect or for it's own 'valueChanges' so it's indeed undefined when the component tries to access it; and that's why initializing it solves the error log, but messes with the view.

What's the proper way to do this? Thanks!

Edit #1:

When I move my subscribe from the component constructor to ngOnInit it stops rendering

enter image description here

Edit #2:

I began trying things, including leaving open my Firestore, so I deleted the line where I subscribe to authState and It began working. This won't work in production, but I think the problem is in my auth subscription, not where I first tough

constructor(private auth: AngularFireAuth, private db: AngularFirestore) {
// this.auth.authState.subscribe(user => {
    this.uid = "twCTRUpXvYRiYGknXn6j7fe0mvj2";
    this.businessDoc = db.doc<Business>(this.businessAddress);
    this.business = this.businessDoc.valueChanges()
 // });
}

private get businessAddress() {
    return 'users/' + this.uid
}

Solution

  • The problem is that the code as it is written expects the authState to resolve synchronously. That call is asynchronous and thus a call to a state that is set after that resolves will return undefined (or whatever the default/initial value is).

    There are many ways to resolve this, the one I chose is to add an initialize method to the service that returns an observable that can be subscribed to by a component. This will resolve once authState resolves. I used shareReplay so that the call will only ever resolve a single time successfully and will then replay the result to any subsequent subscriptions. The tap operator allows the state to be set on the service without subscribing directly to the observable.


    DatabaseService.ts

    import { tap, shareReplay } from 'rxjs/operators';
    import { Observable } from 'rxjs';
    
    @Injectable({
      providedIn: 'root'
    })
    export class DatabaseService {
      public businessDoc: AngularFirestoreDocument<Business>;
      public business: Observable<Business>;
      private readonly _initialize: Observable<any>;
    
      constructor(private auth: AngularFireAuth, private db: AngularFirestore) {
        this._initialize = this.auth.authState.pipe(tap(user => {
            this.uid = user.uid;
            this.businessDoc = this.db.doc<Business>(this.businessAddress);
            this.business = this.businessDoc.valueChanges()
        }), shareReplay());
      }
    
      initialize() : Observable<any> {
        return this._initialize;
      }
    
      private get businessAddress() {
        return 'users/' + this.uid
      }
    }
    

    ConfigComponent.ts

    export class ConfigComponent implements OnInit {
    
      config: Config;
    
      constructor(private readonly db: DatabaseService){ }
    
      ngOnInit() {
        this.db.initialize().subscribe(() => {
          this.db.business.subscribe(res => {
            this.config = res.config;
          });
        });
      }