Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Alex Fallenstedt
Alex Fallenstedt

Posted on • Edited on

     

Scan Operator For Mini Redux Stores

toaster

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.

toast

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;}}}}
Enter fullscreen modeExit fullscreen mode

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});}...
Enter fullscreen modeExit fullscreen mode

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;}}}}
Enter fullscreen modeExit fullscreen mode

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');}...
Enter fullscreen modeExit fullscreen mode
<!-- toast.component.html --><button*ngFor="let toast of (state$ | async)"(click)="remove(toast)"><span[innerHtml]="toast.message"></span></button>
Enter fullscreen modeExit fullscreen mode

And what you get are many toast notifications with a powerful service with a very nice API.

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

Senior Software Engineer at New Relic. Building findtechjobs.io
  • Location
    Portland, OR
  • Work
    Senior Software Engineer at New Relic
  • Joined

More fromAlex Fallenstedt

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