cpeddie
cpeddie

Reputation: 799

subscribe to observable but data not ready when run

I'm having trouble synchronizing data from an observable in a service with a component that consumes the data. The data service calls an api service when it is created to build a list of devices it gets from a server. This appears to be working fine as the device list is built in the data service.

import { Injectable } from '@angular/core';
import { ApiService } from './api.service';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class DeviceManagerService {
  devicesInfo = null;
  deviceInfo = null;
  devices = [];

  constructor(private apiService: ApiService ) { 
    this.apiService.getDeviceStatus().subscribe((resp) => {
      this.devicesInfo = resp;
      console.log('DeviceManager: deviceInfo: ',this.devicesInfo);
      this.buildDeviceTable(resp);
      console.log('devices[]=', this.devices);
    }); 
  }

  buildDeviceTable(devicesInfo) {

    devicesInfo.record.forEach( device => {
      console.log('record.devid= ', device.devid);
      if ( this.devices.indexOf(device.devid) > -1) {
        //console.log('element ', device.devid, ' already in devices array');
      }
      else {
        //this.devices.push({ device: device.devid });
        this.devices.push(device.devid);
        //console.log('added ', device.devid, ' to devices array');
      }

    })
  }

  getDevices(): Observable<string[]> {
    let data = new Observable<string[]>(observer => {
      observer.next(this.devices);
    });
    return data;
  }
}

In my component I want to display that list of devices in a mat-table. I have the list of devices set up as an observable in the data service and the component subscribes to that. But when the subscribe function is run, the data is not returned in the observable- the array is empty and the forEach loop does not run to copy the device IDs from the observable into the local array that is used as the data source fo the mat-table.

import { DeviceManagerService } from './../../services/device-manager.service';
import { Component, OnInit } from '@angular/core';
import { MatTableModule, MatTableDataSource } from '@angular/material';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-devices',
  templateUrl: './devices.component.html',
  styleUrls: ['./devices.component.css']
})
export class DevicesComponent implements OnInit {
  displayedColumns: string[] = ['deviceID'];
  //devices = null;
  devices=["test1","test2"];
  deviceData = null;

  constructor(private deviceManager: DeviceManagerService) {
    this.deviceManager.getDevices().subscribe( devTable => {
      console.log('devTable: length', devTable.length);
      devTable.forEach( device => {
        console.log('forEach: device: ', device);
      });
      console.log('devices: ', this.devices);
      //this.devices = devTable;
    });   
   }

  ngOnInit() {
    this.deviceData = new MatTableDataSource<any>(this.devices);
    console.log('devicessComponent: ', this.deviceData); 
  }

}

devices remain at their default initialized value and never get assigned the values that come from the device manager. But the device manager has built the proper device list, for some reason it is not getting to the component via the subscribe function.

How do I make sure that the data is provided in the subscribe function?

Thanks....

Upvotes: 0

Views: 1293

Answers (2)

Poul Kruijt
Poul Kruijt

Reputation: 71891

You should use either a ReplaySubject or a BehaviorSubject. The first you will use if you only want it to emit a value when the first value gets emitted. The latter if you want it to have a default value of, for instance, an empty array.

You could even update your service to be completely with streams, which will always keep your data up to date and greatly reduces the use of .subscribe which is a big cause of memory leaks (unclosed subscriptions):

@Injectable({
  providedIn: 'root'
})
export class DeviceManagerService {
  readonly devicesInfo$ = this.apiService.getDeviceStatus().pipe(
    shareReplay(1)
  );

  readonly devices$ = this.devicesInfo$.pipe(
    map((devicesInfo) => devicesInfo.record
      .filter((dev, i, arr) => arr.findIndex(({ devid }) => devid === dev.devid) === i)
    ),
    shareReplay(1)
  );

  constructor(private apiService: ApiService ) {}
}

And because you are using streams now, the use of MatTableDataSource becomes obsolete, as you can just pass the observable as data source to the mat-table:

export class DevicesComponent {
  displayedColumns: string[] = ['deviceID'];

  readonly devices$ = this.deviceManager.devices$;

  constructor(private deviceManager: DeviceManagerService) {}
}

As you can see, I don't use a BehaviorSubject or ReplaySubject, but I do use the shareReplay() operator. Which basically turns an observable in a ReplaySubject and shares the subscriptions amongst subscribers.

Be aware though, that usage of shareReplay(1) should be done with caution. If you have an observable that doesn't complete, the subscription doesn't end, even if the component gets destroyed. You can either add a takeUntil(//destroy observable), or change it to shareReplay({ refCount: true, bufferSize: 1 }). The latter will restart the original Observable once the subscription count hits 0 and is subscribed to again after. The good thing is though, that things really gets cleaned. So.. that's just a footnote :)

Upvotes: 1

SnorreDan
SnorreDan

Reputation: 2890

I am not a 100% sure, but I believe the reason it is not working is that your observable needs to be subscribed to before you pass the data to it. In your code you first pass the data with observer.next(this.devices), then you subscribe. By the time you subscribe the data has already been sent and the event is already finished.

There is a couple of ways this could be solved, one is with a BehaviorSubject.

  1. At the top of your service you add the property devices$ = new BehaviourSubject<string[]>([]);.

  2. At the end of your method 'buildDeviceTable()' you add the line this.devices$.next(this.devices);.

  3. In your component you replace the this.deviceManager.getDevices().subscribe( with this.deviceManager.devices$.subscribe(

Upvotes: 0

Related Questions