DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 963,673 amazing developers

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

Create account Log in
Alfredo Perez
Alfredo Perez

Posted on

NGRX Workshop Notes - Effects

  • Processes that run in the background
  • Connect your app to the outside world
  • Often used to talk to services
  • Written entirely using RxJS streams

Notes

  • Try to keep effect close to the reducer and group them in classes as it seems convenient
  • For effects, it's okay to split them into separate effects files, one for each API service. But it's not a mandate
  • Is still possible to use guards and resolver, just dispatch an action when it is done
  • It is recommended to not use resolvers since we can dispatch the actions using effects
  • Put the books-api.effects file in the same level as books.module.ts, so that the bootstrapping is done at this level and effects are loaded and running if and only if the books page is loaded. If we were to put the effects in the shared global states, the effects would be running and listening at all times, which is not the desired behavior.
  • An effect should dispatch a single action, use a reducer to modify state if multiple props of the state need to be modified
  • Prefer the use of brackets and return statements in arrow function to increase debugability
// Prefer this
getAllBooks$ = createEffect(() => {
    return this.actions$.pipe(
        ofType(BooksPageActions.enter),
        mergeMap((action) => {
            return this.booksService
                .all()
                .pipe(
                    map((books: any) => BooksApiActions.booksLoaded({books}))
                )
        })
    );
})

// Instead of 
 getAllBooks$ = createEffect(() =>
    this.actions$.pipe(
       ofType(BooksPageActions.enter),
       mergeMap((action) =>
           this.booksService
               .all()
               .pipe(
                   map((books: any) => BooksApiActions.booksLoaded({books}))
               ))
    ))

Enter fullscreen mode Exit fullscreen mode

What map operator should I use?

switchMap is not always the best solution for all the effects and here are other operators we can use.

  • mergeMap Subscribe immediately, never cancel or discard. It can have race conditions.

This can be used to Delete items, because it is probably safe to delete the items without caring about the deletion order

deleteBook$ = createEffect(() =>
        this.actions$.pipe(
            ofType(BooksPageActions.deleteBook),
            mergeMap(action =>
                this.booksService
                    .delete(action.bookId)
                    .pipe(
                        map(() => BooksApiActions.bookDeleted({bookId: action.bookId}))
                    )
            )
        )
    );
Enter fullscreen mode Exit fullscreen mode
  • concatMap Subscribe after the last one finishes

This can be used for updating or creating items, because it matters in what order the item is updated or created.

createBook$ = createEffect(() =>
    this.actions$.pipe(
        ofType(BooksPageActions.createBook),
        concatMap(action =>
            this.booksService
                .create(action.book)
                .pipe(map(book => BooksApiActions.bookCreated({book})))
        )
    )
);
Enter fullscreen mode Exit fullscreen mode
  • exhaustMap Discard until the last one finishes. Can have race conditions

This can be used for non-parameterized queries. It does only one request event if it gets called multiple times. Eg. getting all books.

getAllBooks$ = createEffect(() => {
    return this.actions$.pipe(
        ofType(BooksPageActions.enter),
        exhaustMap((action) => {
            return this.booksService
                .all()
                .pipe(
                    map((books: any) => BooksApiActions.booksLoaded({books}))
                )
        })
    )
})
Enter fullscreen mode Exit fullscreen mode
  • switchMap Cancel the last one if it has not completed. Can have race conditions

This can be used for parameterized queries

Other effects examples

  • Effects does not have to start with an action
@Effect() tick$ = interval(/* Every minute */ 60 * 1000).pipe(
 map(() => Clock.tickAction(new Date()))
);
Enter fullscreen mode Exit fullscreen mode
  • Effects can be used to elegantly connect to a WebSocket
@Effect()
ws$ = fromWebSocket("/ws").pipe(map(message => {
  switch (message.kind) {
    case β€œbook_created”: {
      return WebSocketActions.bookCreated(message.book);
    }
    case β€œbook_updated”: {
      return WebSocketActions.bookUpdated(message.book);
    }
    case β€œbook_deleted”: {
      return WebSocketActions.bookDeleted(message.book);
     }
}}))
Enter fullscreen mode Exit fullscreen mode
  • You can use an effect to communicate to any API/Library that returns observables. The following example shows this by communicating with the snack bar notification API.
@Effect() promptToRetry$ = this.actions$.pipe(
 ofType(BooksApiActions.createFailure),
 mergeMap(action =>
    this.snackBar
        .open("Failed to save book.","Try Again", {duration: /* 12 seconds */ 12 * 1000 })
        .onAction()
        .pipe(
          map(() => BooksApiActions.retryCreate(action.book))
        )
   )
);
Enter fullscreen mode Exit fullscreen mode
  • Effects can be used to retry API Calls
@Effect()
createBook$ = this.actions$.pipe(
 ofType(
    BooksPageActions.createBook,
    BooksApiActions.retryCreate,
 ),
 mergeMap(action =>
   this.booksService.create(action.book).pipe(
     map(book => BooksApiActions.bookCreated({ book })),
     catchError(error => of(BooksApiActions.createFailure({
       error,
       book: action.book,
     })))
 )));
Enter fullscreen mode Exit fullscreen mode
  • It is OK to write effects that don't dispatch any action like the following example shows how it is used to open a modal
@Effect({ dispatch: false })
openUploadModal$ = this.actions$.pipe(
 ofType(BooksPageActions.openUploadModal),
 tap(() => {
    this.dialog.open(BooksCoverUploadModalComponent);
 })
);
Enter fullscreen mode Exit fullscreen mode
  • An effect can be used to handle a cancelation like the following example that shows how an upload is cancelled
@Effect() uploadCover$ = this.actions$.pipe(
 ofType(BooksPageActions.uploadCover),
 concatMap(action =>
    this.booksService.uploadCover(action.cover).pipe(
      map(result => BooksApiActions.uploadComplete(result)),
      takeUntil(
        this.actions$.pipe(
          ofType(BooksPageActions.cancelUpload)
        )
))));
Enter fullscreen mode Exit fullscreen mode

Top comments (0)

Classic DEV Post from 2020:

js visualized

πŸš€βš™οΈ JavaScript Visualized: the JavaScript Engine

As JavaScript devs, we usually don't have to deal with compilers ourselves. However, it's definitely good to know the basics of the JavaScript engine and see how it handles our human-friendly JS code, and turns it into something machines understand! πŸ₯³

Happy coding!