Search code examples
angulartypescriptrxjs-observables

What is the best practice to share data in map/object/array by service among Angular components


I am studying angular now. When it comes to data sharing using service, many are recommending that we should use Subject like BehaviorSubject. But should we do it all the time?

@Injectable({
  providedIn: 'root'
})
export class DataService {

  private imageUrls = new Map<number, string>();
  public imageUrlsSubject = new BehaviorSubject(this.imageUrls);

   public size(): number {
     return this.imageUrls.size;
   }

   public set(id: number, url: string): void {
     this.imageUrls.set(id, url);
     this.imageUrlsSubject.next(this.imageUrls);
   }

   public get(id: number): string {
     return this.imageUrls.get(id);
    }


    public has(id: number): boolean {
      return this.imageUrls.has(id);
    }
}

I wrote a service like this following this practice. It makes me wonder: Do we really need Subject here? Can I just get rid of imageUrlsSubject under the context that another component just need to use the map in the DOM.

<img   
  *ngIf="dataService.has(row.id)" 
  [src]="dataService.get(row.id).url">

And in another component maybe I just need to call dogDataService.set to update the map.

this.dataService.set(id, url);

If I get rid of Subject, will it bring some potential drawbacks here?

Thanks!


Solution

  • You are only using your BehaviorSubject internally, but your getter does not rely on it. Your current service is completely synchronous to its consumers and "yes", the way you programmed it now the imageUrlsSubject isn't really necessary.

    The true advantage of using a Subject lies in the fact that you can provide this as an Observable to other places! Instead of people only being able to access the current value of imageUrls they can get access to the current and all future values of imageUrls! They are given a stream instead of a snapshot.

    You then have 3 design options:

    • Always return an Observable. People have to pipe it and take(1) if they want a snapshot.
    • Always return an Observable but add a shorthand function snap that returns the current value.
    • Always return a snapshot (as it currently does in your code) and add a watch function that returns an Observable.

    I would go even further and say that you shouldn't keep a seperate imageUrls property and instead rely on imageUrlsSubject.value (only for BehaviorSubjects). I'm only recommending this so you A) are less likely to forget subject.next(newValue) when you change your value B) to remind you again that you're supposed to use your Subject ;p

    Here is the adapted example that maintains the default synchronous behavior of get and adds an asynchronous watch.

    @Injectable({
      providedIn: 'root'
    })
    export class DataService {
      public imageUrlsSubject = new BehaviorSubject(new Map<number, string>());
    
       public size(): number {
         return this.imageUrlsSubject.value.size;
       }
    
       public set(id: number, url: string): void {
         this.imageUrlsSubject.value.set(id,url);
         this.imageUrlsSubject.next(this.imageUrlsSubject.value);
       }
    
       public get(id: number): string {
         return this.imageUrlsSubject.value.get(id);
        }
    
    
        public has(id: number): boolean {
          return this.imageUrlsSubject.value.has(id);
        }
    
        public watch(): Observable<Map<number, string>> {
          return this.imageUrlsSubject.asObservable();
        }
    }
    

    Now elsewhere in your code you can subscribe to any changes in your map, you can pipe it, pass the pipe/observable around, ...