Search code examples
javascriptangulartypescriptngx-leaflet

How to properly multicast result of HttpClient request to several components in Angular?


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

  • wrap my call to next() in the service in NgZone.run(() => { this.response.next(resp); }
  • call 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.


Solution

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