I was placed on an Angular project that did not have a state management system like Redux or ngrx. I saw this as an opportunity to gently introduce state management using RxJS.
There aren+1
blog posts about Reactive Programming. In a nutshell, reactive programming concerns itself with async data. This data can come from APIs, or from user events.
The task was to build a toast notification system. Something similar to Angular Material’s Snackbar. The requirements were:
- each toast notification auto-expires
- each toast notification can be closed ahead of time by a user
- and there can be many toast notifications at once.
I ended up using thescan
operator from RxJS to have data persistence. You can think of a scan operator as JavaScript'sreduce
method.
This was accomplished inside an Angular service, however, you can take these concepts in any project that uses RxJS.
// toast.service.ts// getter so no bad developer uses the store incorrectly.getstore(){returnthis._store$;}// Dispatch "actions" with a subjectprivate_action$=newSubject();// Create a store which is just an array of 'Toast'private_store$:Observable<Toast[]>=this._action$.pipe(map((d:ToastAction)=>(!d.payload.id)?this.addId(d):d),// add id to toast to keep track of them.mergeMap((d:ToastAction)=>(d.type!==ToastActionType.Remove)?this.addAutoExpire(d):of(d)),// concat a hide toast request with delay for auto expiringscan(this.reducer,[])// magic is here!);// dispatch methodpublicdispatch(action:ToastAction):void{this._action$.next(action);}// generate ids for the toastprivateaddId(d:ToastAction):ToastAction{return({type:d.type,payload:{...d.payload,id:this.generateId()}});}// If a user does not click on the toast to clear it, then it should auto expireprivateaddAutoExpire(d:ToastAction){constsignal$=of(d);consthide$=of({type:ToastActionType.Remove,payload:d.payload}).pipe(delay(this.config.duration));returnconcat(signal$,hide$);}// generates a random stringprivategenerateId():string{return'_'+Math.random().toString(36).substr(2,9);}// The reducer which adds and removes toast messages.privatereducer(state:Toast[]=[],action:ToastAction):Toast[]{switch(action.type){caseToastActionType.Add:{return[action.payload,...state];}caseToastActionType.Remove:{returnstate.filter((toast:Toast)=>toast.id!==action.payload.id);}default:{returnstate;}}}}
Being able to use scan was perfect for this situation. I could reduce incoming streams of data into an array of objects using the reducer function.
To use the store, you can either subscribe to thestore
or reference it in your Angular template.
// toast.component.tsexportclassToastComponent{publicstate$:Observable<Toast[]>=this.toastService.store;constructor(publictoastService:ToastService){}publicadd(){this.toastService.dispatch({type:ToastActionType.Add,payload:{message:'hi',status:ToastStatus.Info}});}publicremove(payload:Toast){this.toastService.dispatch({type:ToastActionType.Remove,payload});}...
But is this ideal? Yes it works, however, a developer that comes by shouldn't have to care aboutToastActionType
and construction objects. It's a lot of work. Let's create some helper methods in our ToastService so any developer can call 'enqueueSuccess' or 'enqueueInfo' for the type of toast we want.
// toast.service.ts@Injectable({providedIn:'root'})exportclassToastService{getstore(){returnthis.store$;}privateaction$:Subject<ToastAction>=newSubject<ToastAction>();privatestore$:Observable<Toast[]>=this.action$.pipe(map((d:ToastAction)=>(!d.payload.id)?this.addId(d):d),mergeMap((d:ToastAction)=>(d.type!==ToastActionType.Remove)?this.addAutoExpire(d):of(d)),scan(this.reducer,[]));constructor(){}publicenqueueSuccess(message:string):void{this.action$.next({type:ToastActionType.Add,payload:{message,status:ToastStatus.Success}});}publicenqueueError(message:string):void{this.action$.next({type:ToastActionType.Add,payload:{message,status:ToastStatus.Error}});}publicenqueueInfo(message:string):void{this.action$.next({type:ToastActionType.Add,payload:{message,status:ToastStatus.Info}});}publicenqueueWarning(message:string):void{this.action$.next({type:ToastActionType.Add,payload:{message,status:ToastStatus.Warning}});}publicenqueueHide(payload:Toast):void{this.action$.next({type:ToastActionType.Remove,payload});}privateaddId(d:ToastAction):ToastAction{return({type:d.type,payload:{...d.payload,id:this.generateId()}});}privateaddAutoExpire(d:ToastAction){constsignal$=of(d);consthide$=of({type:ToastActionType.Remove,payload:d.payload}).pipe(delay(this.config.duration));returnconcat(signal$,hide$);}privategenerateId():string{return'_'+Math.random().toString(36).substr(2,9);}privatereducer(state:Toast[]=[],action:ToastAction):Toast[]{switch(action.type){caseToastActionType.Add:{return[action.payload,...state];}caseToastActionType.Remove:{returnstate.filter((toast:Toast)=>toast.id!==action.payload.id);}default:{returnstate;}}}}
Now our component has a much easier time calling our service:
// toast.component.tsexportclassToastComponent{publicstate$:Observable<Toast[]>=this.toastService.store;constructor(publictoastService:ToastService){}publicadd(){this.toastService.enqueueInfo('It works!');this.toastService.enqueueWarning('Oops!');this.toastService.enqueueError('Uh oh!');this.toastService.enqueueSucccess(':D');}...
<!-- toast.component.html --><button*ngFor="let toast of (state$ | async)"(click)="remove(toast)"><span[innerHtml]="toast.message"></span></button>
And what you get are many toast notifications with a powerful service with a very nice API.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse