Best practices for generic effects #3428
-
Hi everyone! The team I am working on, are adopting some NgRx best practices, but we have some doubts about a particular case: Simplified version: We have a home page that loads some products. If the load fails, we need to display a feedback message. When the Home Page is loaded the ngOnInit function dispatches the action:
And we have this effect to handle it: loadProducts$ = createEffect(() => {
return this.actions$.pipe(
ofType(homePageActions.opened),
exhaustMap(() => {
return this.products.loadProducts().pipe(
map(products => productsApiActions.productsLoadedSuccess({ products })),
catchError((error) => of(productsApiActions.productsLoadedFailure({ error })))
)
})
);
}); To display the error message we have the NotificationsModule with this effect: showErrorToastr$ = createEffect(() =>
this.actions$.pipe(
ofType(NotificationsActions.showError),
tap(({ message }) => this.notifier.showError(message)),
),
{ dispatch: false }
); And we map the failure action to showError: showLoadProductsError$ = createEffect(() => {
return this.actions$.pipe(
ofType(productsApiActions.productsLoadedFailure),
map(({error}) => NotificationsActions.showError({ message: error.message }))
)
}); But with this approach we are not following these best practices:
To follow these practices, we need to change the effect from the NotificationsModule to: showErrorToastr$ = createEffect(() =>
this.actions$.pipe(
ofType(
...other actions...
productsApiActions.productsLoadedFailure),
tap(({ error }) => this.notifier.showError(error.message)),
),
{ dispatch: false }
); But this way, NotificationsModule need to know every failure action from other modules and we don't want to do that. We don't really see any benefits in doing that way. What would be the best way to follow the best practices in that particular case? |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 5 replies
-
I recommend following an "action metadata" pattern to drive side-effects like showing notifications. Please check out my article here: https://dev.to/davidshortman/describe-your-ngrx-actions-more-to-write-less-code-1fij In your instance, you could insert metadata like so: export const productsLoadedFailure = createAction(
'[Home Page] Products loaded failure',
(payload: { error: { message: string } }) => ({
...payload,
notificationMetadata: { message }
})
); Then you could generically react to actions with notification metadata: showErrorToast$ = createEffect(() =>
this.actions$.pipe(
filter(hasNotificationMetadata),
tap(({ notificationMetadata: { message } }) => this.notifier.showError(message)),
),
{ dispatch: false }
); |
Beta Was this translation helpful? Give feedback.
-
@MikelEiza one way we have solved this issue in our project is that we created a function that creates actions for HTTP calls with specific parameters, and then we wrote one global effect that handles certain types of actions (the error or success ones). Here is the function we use to create actions: export function createHTTPActions<
RequestPayload = void,
ResponsePayload = void,
ErrorPayload = Error,
>(
actionType: string,
): [
ActionCreator<
string,
(props: RequestPayload) => {
payload: RequestPayload;
} & TypedAction<string>
>,
ActionCreator<
string,
(
message: string,
props?: ResponsePayload,
showMessage?: boolean,
customTitle?: string,
) => {
payload: ResponsePayload;
success: boolean;
message: string;
showMessage: boolean;
customTitle: string;
} & TypedAction<string>
>,
ActionCreator<
string,
(
props: ErrorPayload,
showMessage?: boolean,
) => {
payload: ErrorPayload;
success: boolean;
showMessage: boolean;
} & TypedAction<string>
>,
] {
return [
createAction(actionType, (payload: RequestPayload) => ({ payload })),
createAction(
`${actionType} Success`,
(
message: string,
payload?: ResponsePayload,
showMessage = false,
customTitle = '',
) => ({
payload,
message,
success: true,
showMessage,
customTitle,
}),
),
createAction(
`${actionType} Error`,
(payload: ErrorPayload, showMessage = true) => ({
payload,
success: false,
showMessage,
}),
),
];
} It's a bit of a mouthful, but when creating actions for HTTP calls, it makes life really easy: export const [downloadAttachment, downloadAttachmentSuccess, downloadAttachmentError] = createHTTPActions<
{
type: number;
attachmentInfo: { attachmentId: number; attachmentName: string };
},
HttpResponse<Blob>
>('[API] Download Attachment'); Now we have all the three action we might need for this, and we can then handle the error notification with one effect: @Injectable()
export class GlobalEffects {
errorMessage$ = createEffect(
() => {
return this.actions$.pipe(
tap(
(
action: Action & {
success: boolean;
payload: Error;
showMessage: boolean;
},
) => {
if (action.success === false) {
this.toastService.rror(action.message);
}
},
),
);
},
{ dispatch: false },
);
constructor(
private readonly actions$: Actions,
private readonly toastService: ToastService,
) {}
} This is of course just one approach to handling these cases, but it has worked magically for us, never required any maintenance, and whenever we wanted to change some logic, it was super easy |
Beta Was this translation helpful? Give feedback.
I recommend following an "action metadata" pattern to drive side-effects like showing notifications.
Please check out my article here: https://dev.to/davidshortman/describe-your-ngrx-actions-more-to-write-less-code-1fij
In your instance, you could insert metadata like so:
Then you could generically react to actions with notification metadata: