Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Understanding Multicasting Observables in Angular
Bitovi profile imageJennifer Wadella
Jennifer Wadella forBitovi

Posted on • Edited on • Originally published atjenniferwadella.com

     

Understanding Multicasting Observables in Angular

Many times in Angular application development we'll have an Observable, and want to use the values from that Observable to do different things in the UI.

Let's imagine we're building this interface that shows information about a fish, and we want to show users a schedule of when that fish is available based on the hemisphere of the world selected.

Fish Display UI

In our component we'll get the response of an HTTP request to the animal crossing API. We're using HTTPClient which returns an Observable. We want to display data from that HTTP request in our UI so a user can see information about the fish, but we also want to display a custom built schedule based on that data and input from something else.

The API returns an object that looks something like this:

{"id":10,"fileName":"killifish","name":{"name-en":"killifish",...},"availability":{"month-northern":"4-8","month-southern":"10-2","time":"","isAllDay":true,"isAllYear":false,"location":"Pond","rarity":"Common"},"shadow":"Smallest (1)","price":300,"priceCj":450,"catchPhrase":"I caught a killifish! The streams are safe again.","museumPhrase":"Ah, the magnificent killifish! Did you know there are over 1,000 different species? My own favorite killifish species are the mangrove, which can live on land for weeks, breathing air! And the mummichog, the first fish ever brought to space. I wonder if the killifish you've brought me is related to either those intrepid explorers?",}
Enter fullscreen modeExit fullscreen mode

We want to get the availability based on the hemisphere(northern or southern) the user cares about, and display the months during which that fish is available, by creating an array like this:

[{"month":"January","available":false},{"month":"February","available":true},...]
Enter fullscreen modeExit fullscreen mode

We might consider doing something like this (note we are using the Async pipe in our component template to subscribe tofish$):

// fish.component.tspublicfish$:Observable<Fish&{uiSchedule:Schedule}>;publicselectedHemi=newBehaviorSubject<'northern'|'southern'>('northern');publicdisplayedSchedule$:Observable<Month[]>;constructor(privateroute:ActivatedRoute,privateacnhService:AcnhService){}ngOnInit():void{this.fish$=this.route.paramMap.pipe(switchMap((params:ParamMap)=>{returnthis.acnhService.getFish(params.get('id')).pipe(map((res:Fish)=>{return{...res,uiSchedule:{// mapping function to generate array of months with key of// whether month is available or notnorthern:buildSchedule(res.availability,'northern'),southern:buildSchedule(res.availability,'southern')}}}));}),)this.displayedSchedule$=this.selectedHemi.pipe(withLatestFrom(this.fish$),map(([selectedHemi,fish])=>{returnfish.uiSchedule[selectedHemi];}))}
Enter fullscreen modeExit fullscreen mode
// fish.component.html<mat-card*ngIf="fish$ | async as fish"color="secondary"><mat-card-header><mat-card-title>{{fish.name['name-en']}}</mat-card-title><mat-card-subtitle>{{fish.price | currency }}</mat-card-subtitle></mat-card-header><mat-card-contentclass="row"><div><imgsrc="{{fish.imageUrl}}"alt="{{fish.name['name-en']}}"><blockquoteclass="museum-phrase">"{{fish.museumPhrase}}"</blockquote></div><div><mat-button-toggle-groupname="hemisphere"[value]="selectedHemi | async"aria-label="Hemisphere"color="primary"(change)="selectedHemi.next($event.value)"><mat-button-togglevalue="northern">Northern Hemisphere</mat-button-toggle><mat-button-togglevalue="southern">Southern Hemisphere</mat-button-toggle></mat-button-toggle-group><divclass="table display-availability"><divclass="month"*ngFor="let month of displayedSchedule$ | async"[ngClass]="{'available':month.available}">          {{month.month}}</div></div><div*ngIf="fish.availability.isAllDay;else limitedHours"><p>The {{fish.name['name-en']}} is available at all times</p></div><ng-template#limitedHours><p>The {{fish.name['name-en']}} is available from {{fish.availability.time}}</p></ng-template></div></mat-card-content></mat-card>
Enter fullscreen modeExit fullscreen mode

This will give us adisplayedSchedule$ Observable with an array that displays either the northern or southern hemisphere schedule when the value ofselectedHemi changes. Again, assume that we're using the Async pipe in our template to subscribe to this Observable because we want the tear down functionality of our Angular component to handle unsubscribing for us.

But by doing this we're creating an additional subscription tofish$ when we subscribe todisplayedSchedules, which means our Observable is being executed twice, quite unnecessarily. Not to mention rude, this awesome developer built a great free API indexing Animal Crossing stuff, and we're thoughtlessly hitting it twice? Ruuuuuude. (ps. how many of ya'll have been doing something like this without even realizing?)

How can we avoid this?

Instead of anObservable, we can use aSubject instead. Subjects can have multiple subscribers and only execute their context once. To convert an Observable to a Subject we can use themulticast operator.

The multicast operator is a bit of a bear to understand - it takes a selector as a parameter and according to the docs returns

"An Observable that emits the results of invoking the selector on the
items emitted by a ConnectableObservable that shares a single
subscription to the underlying stream."

A more palatable summary from the docs is

"A multicasted Observable uses a Subject under the hood to make multiple Observers see the same Observable execution. Multicast returns an Observable that looks like a normal Observable, but works like a Subject when it comes to subscribing. multicast returns a ConnectableObservable, which is simply an Observable with the connect() method.

The connect() method is important to determine exactly when the shared Observable execution will start. Because connect() does source.subscribe(subject) under the hood, connect() returns a Subscription, which you can unsubscribe from in order to cancel the shared Observable execution.

So let's pipe the multicast operator to source Observablefish$ with a newReplaySubject (because we want late subscribers to get the value).


On the Subject of Subjects ...

subject - a special type of Observable that allows values to be multicasted to many Observers

behaviorSubject - a subject that can 'store' a current value that new subscribers will receive

replaySubject - a subject than can send old values to new subscribers


this.fish$=this.route.paramMap.pipe(switchMap((params:ParamMap)=>{returnthis.acnhService.getFish(params.get('id')).pipe(map((res:Fish)=>{return{...res,uiSchedule:{northern:buildSchedule(res.availability,'northern'),southern:buildSchedule(res.availability,'southern')}}}));}),multicast(newReplaySubject(1)))
Enter fullscreen modeExit fullscreen mode

... now we have nothing displaying in our UI? Why? We still have the async pipe subscribing tofish$, butfish$ is now a ConnectableObservable, and we must call theconnect method on it to trigger our source observables execution.

// RxJS source codefunctionMulticast(){...return<ConnectableObservable<R>>connectable;}exportclassConnectableObservable<T>extendsObservable<T>{...connect():Subscription{letconnection=this._connection;if(!connection){this._isComplete=false;connection=this._connection=newSubscription();connection.add(this.source.subscribe(newConnectableSubscriber(this.getSubject(),this)));if(connection.closed){this._connection=null;connection=Subscription.EMPTY;}}returnconnection;}refCount():Observable<T>{returnhigherOrderRefCount()(this)asObservable<T>;}...}
Enter fullscreen modeExit fullscreen mode
this.fish$.connect()
Enter fullscreen modeExit fullscreen mode

However, this means we must also remember to unsubscribe from that subscription created by the connect method, so doesn't that defeat the purpose of using the async pipe? Yep. Boo. BUT, fear not, gentle reader, we can use therefCount operator, instead of having to manage theconnect method ourselves.

refCount makes the multicasted Observable automatically start executing when the first subscriber arrives, and stop executing when the last subscriber leaves.

RefCount returns an Observable that keeps track of how many subscribers it has, it will start executing when subscribers is more than 0, and stops when subscribers are 0 again. This means when we use our async pipe onfish$, the count will become 1, when we use our async pipe ondisplayedSchedule$ the count will become 2 and when our component is destroyed and the async pipes unsubscribe, the count will go back to 0.

Our final code looks something like this

this.fish$=this.route.paramMap.pipe(switchMap((params:ParamMap)=>{returnthis.acnhService.getFish(params.get('id')).pipe(map((res:Fish)=>{return{...res,uiSchedule:{northern:buildSchedule(res.availability,'northern'),southern:buildSchedule(res.availability,'southern')}}}));}),multicast(newReplaySubject(1)),refCount())
Enter fullscreen modeExit fullscreen mode

In summary, when we have an Observable we'd like to use a source for various purposes without executing its context every time, we can use themulticast operator to take a Subject and use it to share the source execution of our source Observable. The multicast operator returns a ConnectableObservable type, on which we CAN use theconnect method to create the subscription to our source Observable(the HTTP request to get a fish). A more manageable approach is to use the refCount operator which will count subscriptions and call theconnect method to subscribe to the source Observable once the first subscription is created and run tear down logic when the subscription count returns to 0 (AKA all the subscriptions have been unsubscribed).

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Perfecting digital products.

Join our Community Discord ⇩

More fromBitovi

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp