LittleFish
LittleFish

Reputation: 101

Get values from an array received from subject

I have a project in Ionic5 that request some itens to an api. I have a similar code as below:

Item.service.ts

getItens(): Observable<Item[]> {
    const subject = new Subject<Item[]>();
    this.http.get<Response>('host').subscribe(data => {
        // Some cool things
        subject.next(data.items)
    }, error => {
        subject.error(error.message)
    });

    return subject.asObservable();
}

items.page.ts

items: Item[] = [];

    loadItems() {
        this.itemService.getItems().subscribe(items => this.items = items);
    }

In a page, I have an interval that update the items. Everything works fine if I use this.items = items but the list disappears and reappears every load, probably because the entire value of this.items is changing

So I tried to just insert differents values in array:

items: Item[] = [];

    loadItems() {
        this.itemService.getItems().subscribe(items => {
            items.forEach(item => {
                const value = this.items.find(i => i.id == item.id);
                if(!value) this.items.unshift(item);
            })
        })
    }

Unfortunately the subject doesn't await the results from service so just an empty array is rendered in the page. An ugly solution that I found was use a timeout to await the results, but the ms has to change based on length of item list. An observation is that if I log items from item.page.ts it will be print an array with length 0, but with values

enter image description here

I'd like to avoid Promises itself and keep Observables if it's possible.
I cannot return the request directly, because I need to manipulate the data.

Is there any way to await subject.next() to render the items? If not, what logic can I use to show the item list?

SOLVED

The problem was solved using a specific logic of the project. But all answers below will be works in normal environments and has a lot of information in the comments. I really recommend read all of them.

Upvotes: 0

Views: 1693

Answers (3)

cjd82187
cjd82187

Reputation: 3593

Like Edward said, you don't need that subject.

// Move your "cool things" and all the other stuff in that subscription into a pipe
getItens(): Observable<Item[]> {

retrun this.http.get<Response>('host').pipe(
       map(data => {
        // Some cool things
        return data.items
      }),
      catchError(error => {
       // if you need to only pass error message you can do it here but you could probably skip this. Make sure you re-thow the error though, so other subscribers see it as an error and not just a valid result.
       return throwError(error.message);
      })
);
}

As for your issue of the whole list getting re-rendered, *ngFor has a trackBy parameter which will help with this. You provide a function, and it helps angular know which items were changed in the array and to only re-render those.

The angular docs have the trackBy syntax https://angular.io/api/common/NgForOf

It would be something like this template:

<my-component *ngFor="let item of items; trackBy: trackByFn> </my-component>

Component

trackByFn(index, item) {
    return item.id // or whatever the key property is
  }

Upvotes: 1

Eric Aska
Eric Aska

Reputation: 646

Well first I guess that you have a memory leak in you Item.service.ts file because every time that you call loadItems() you are subscribing and not unsubscribing for this you can use pipe(first()) and also getItems()function creates a Subject on each call.

Here problem of refreshing is when it replaces the array as you said. although you can check each element of array to see if it's updated or not.

seems lodash has such a function to check equality of two objects

https://www.samanthaming.com/tidbits/33-how-to-compare-2-objects/

Also to test it:

https://stackblitz.com/edit/angular-lodash-tdggzs?file=app%2Fapp.component.ts

And the code:

import * as _ from 'lodash';

export class SampleService {
  items: Item[] = [];

  constructor(private http: HttpClient) {}

  get getItems(): Item[] {
    return this.items;
  }

  fetchData(): void {
    this.http
      .get<Item[]>('host')
      .pipe(first())
      .subscribe((newArray) => {
        this.updateMyArray(newArray);
      });
  }

  updateMyArray(newArray: Item[]) {
    newArray.forEach((item) => {
      const itemInLocalArray = this.items.find((x) => x.id === item.id);
      if (itemInLocalArray) {
        const isItSame = _.isEqual(itemInLocalArray, item);
        if (!isItSame) {
          this.items[this.items.indexOf(itemInLocalArray)] = item;
        }
      } else {
        this.items.push(item);
      }
    });
  }

}

so in code above you create array inside service to save the data and there is no need for Subject.

and to get the data you can first connect to array inside service with

this.items = this.sampleService.getItems;

and call

this.sampleService.fetchData() to fetch data from server and because items array is synchronous and also available, you can use it whenever you want.

 // Inside Component
  items: Item[] = [];
  constructor(private sampleService: SampleService) {
    this.items = this.sampleService.getItems;
  }
  
  fetchData(){
    this.sampleService.fetchData();
  }

Upvotes: 1

Edward
Edward

Reputation: 1126

you don't need a subject in your getItems() service. the http client methods return observables by default

getItems(): Observable<Item[]> {
  return this.http.get<Response>('host');
}

with that, your component would then be the one that does the subscribing.

loadItems() {
    this.itemService.getItems().subscribe(items => this.items = items);
}

by returning the observable from the service, and moving the subscription to the component, when you subscribe to the method, you will be able to set items when the HTTP request completes

Upvotes: 1

Related Questions