I'm trying to send the result of HttpClient
post
requests multiple components in my Angular app. I'm using a Subject
and calling its next()
method whenever a new post
request is successfully executed. Each component subscribes to the service's Subject
.
The faulty services is defined as
@Injectable()
export class BuildingDataService {
public response: Subject<object> = new Subject<object>();
constructor (private http: HttpClient) { }
fetchBuildingData(location) {
...
this.http.post(url, location, httpOptions).subscribe(resp => {
this.response.next(resp);
});
}
The components subscribe to BuildingService.response
as follows
@Component({
template: "<h1>{{buildingName}}</h1>"
...
})
export class SidepanelComponent implements OnInit {
buildingName: string;
constructor(private buildingDataService: BuildingDataService) { }
ngOnInit() {
this.buildingDataService.response.subscribe(resp => {
this.buildingName = resp['buildingName'];
});
}
updateBuildingInfo(location) {
this.buildingDataService.fetchBuildingData(location);
}
}
updateBuildingInfo
is triggered by users clicking on a map.
Retrieving the data from the server and passing it to the components works: I can output the payloads to the console in each component. However, the components' templates fail to update.
After Googling and fiddling for most of today I found that this implementation does not trigger Angular's change detection. The fix is to either
next()
in the service in NgZone.run(() => { this.response.next(resp); }
ApplicationRef.tick()
after this.title = resp['title']
in the component. Both solutions feel like dirty hacks for such a trivial use case. There must be a better way to achieve this.
My question therefore is: what is the proper way to fetch data once and send it off to several components?
I'd furthermore like to understand why my implementation escapes Angular's change detection system.
EDIT it turns out I was initiating my call to HttpClient
outside of Angular's zone hence it could not detect my changes, see my answer for more details.
One way is to get an Observable
of the Subject
and use it in your template using async
pipe:
(building | async)?.buildingName
Also, if different components are subscribing to the service at different times, you may have to use BehaviorSubject
instead of a Subject
.
@Injectable()
export class BuildingDataService {
private responseSource = new Subject<object>();
public response = this.responseSource.asObservable()
constructor (private http: HttpClient) { }
fetchBuildingData(location) {
this.http.post(url, location, httpOptions).subscribe(resp => {
this.responseSource.next(resp);
});
}
}
@Component({
template: "<h1>{{(building | async)?.buildingName}}</h1>"
...
})
export class SidepanelComponent implements OnInit {
building: Observable<any>;
constructor(private buildingDataService: DataService) {
this.building = this.buildingDataService.response;
}
ngOnInit() {
}
updateBuildingInfo(location) {
this.buildingDataService.fetchBuildingData(location);
}
}