angulartypescriptasynchronousservicesubscribe

can't iterate over array created in service - async issue?


I create an array of Items like this in my service:

items: IItem[] = [];
...
LoadItems() {
        this.GetItems().subscribe((res) => {
            // console.log(res);
            if (res.status == 200 && res.body.data != undefined) {
                var data = res.body.data;
                this.items.push({
                        Id: data[i].id,
                        IsBlue: data[i].isBlue, //this is a bool
                    });
                }    
            }
            // console.log(this.items.length);
        });
    }

This is called in ngOnInIt in my app.component.ts file. Then I want to iterate over this array of Items like so in item.component.ts:

GetBlueItems(items: IItem[]) {
    var blueitems: IItem[] = [];
    console.log(items); //this returns the array correctly

    for (let i = 0; i < items.length; i++) {
        if (items[i].isBlue) {
            blueitems.push(items[i]);  
        }
    }

But it just never iterates over the array. When I do console.log(items.length) it returns 0, which confuses me because console.log(items) shows that the array is not empty.

I've tried printing the length of the array within the subscribe block and it does return the length correctly, so I assume it's an issue with synchronization? But then I still don't understand why the console would print this without issue:

0: {Id: 1, IsBlue: false }
1: {Id: 2, IsBlue: true }
length: 2

I've also tried doing a forEach loop over the array, but it won't iterate with that either:

items.forEach(item => {
    if (item.isBlue) {
        blueitems.push(item);  
    }
});

I would like to be able to use this array outside of the subscribe block if that even is the issue. I don't need it to be updated constantly.

ETA: The reason I want to be able to use the array outside of the subscribe block is because I want to display the information in html, use it in different functions such as adding items to a shopping cart & checking out, etc. It seems difficult to do if I have to confine my use of the array to within the subscribe block.


Solution

  • I would use RxJS Streams to solve this Problem, i think there is no need to subscribe inside a service. I would just pass the Observable. So Components an use async pipe or signals to pass data to templates.

    interface IItem {
      IsBlue: boolean;
      Id: number;
    }
    
    const fakeResponseData: IItem[] = [
      {
        Id: -1,
        IsBlue: true
      },
      {
        Id: 0,
        IsBlue: false
      }
    ]
    
    const fakeResponse: HttpResponse<IItem[]> = new HttpResponse({
      body: fakeResponseData,
      status: 200,
    })
    
    const fakeRequqest = of(fakeResponse);
    
    @Injectable()
    export class MyService {
    
      private item$: Observable<IItem[]>;
    
      LoadItems(): Observable<IItem[]> {
        if (!this.item$) {
          this.item$ = fakeRequqest.pipe(
            map(response => response.body),
            filter((response): response is IItem[] => response !== null && response !== undefined),
            shareReplay() // shareReplay is optional, it depends on your use case, now we have a little cache
          );
        }
        return this.item$
      }
    }
    
    @Component({
      selector: 'myComponent',
      template: `<ul><li *ngFor="let item of items$ | async">{{item.IsBlue}}</li></ul>`,
      standalone: true,
      imports: [AsyncPipe]
    }) 
    export class myComponent{
    
      private readonly myService = inject(MyService);
    
      protected readonly items$ = this.myService.LoadItems().pipe(
        map(items=> items.filter(item => item.IsBlue))
      )
    
      /** example angular 17+ with signals 
        protected readonly blueItems = toSignal(
          this.myService.LoadItems().pipe(map(items=> items.filter(item => item.IsBlue)))
        );
    
        // in template
        @for (item of blueItems(); track item.id) {
          {{item.IsBlue}}
        }
      */
    }