1

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
}
6
  • 1
    I've deleted my answer regarding your edit :) Commented Mar 5, 2019 at 14:11
  • For 1 thing you should move the logic in the constructor of your ConfigComponent to the method ngOnInit, that is where it belongs. The problem seems that this.businessDoc.valueChanges(); is not returning an observable. Place a breakpoint before/after that line and check the returned value. Commented Mar 5, 2019 at 14:15
  • @igor weirdly when I move this to ngOnInit it stops rendering, and I get the same error Commented Mar 5, 2019 at 14:20
  • do you have a @Injectable() on your DatabaseService? Commented Mar 5, 2019 at 14:28
  • @PierreDuc Yes I do. i have @Injectable({ providedIn: 'root'}) Commented Mar 5, 2019 at 14:32

2 Answers 2

1

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;
      });
    });
  }
Sign up to request clarification or add additional context in comments.

2 Comments

This worked! Thanks, but could you elaborate please? I was under the impression that by subscribing to authState it would resolve asynchronously. Or what would be other methods to solve this?
@Darkade - In your code it did resolve asynchronously but after the constructor of the ConfigComponent (as well as the ngOnInit call) were executed which means that the state of the service was not set at the moment where the ConfigComponent was accessing it. The "new" code allows the component to wait on the state being initialized (ie. the call back from authstate happening).
0

No idea what's going wrong, but let me suggest you another way of putting it down, maybe that will work:

service:

@Injectable({
  providedIn: 'root'
})
export class DatabaseService {
  private businessAddress: string = "business"

  public businessDoc = this.af.doc<Business>(this.businessAddress);
  public business = this.businessDoc.valueChanges();

  constructor(readonly af: AngularFirestore) {}
}

component:

export class ConfigComponent implements OnInit {

  config$: Observable<Config> = this.db.business.pipe(
    map((res) => res && res.config)
  );

  constructor(readonly db: DatabaseService) {}
}

template:

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

1 Comment

Thanks. I tried this, and I realized I neglected to put something in my question that's causing this behavior. I'm also doing some user auth. When I delete my subscribe from auth, i stop seeing the error.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.