- 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}))
))
))
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}))
)
)
)
);
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})))
)
)
);
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}))
)
})
)
})
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())) );
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); } }}))
- 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))
)
)
);
- 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, }))) )));
- 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);
})
);
- 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)
)
))));