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:
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
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
}
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;
});
});
}