DEV Community

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)