Using NgRx for state management has been a great way to store server data across an application. One thing I started to wonder is how could I use this same pattern, but in a different manner to make components even more useful and clean. Luckily, I was just finishing up making some Angular Reactive Forms and needed a way to handle the server errors that came back.
Granted errors are not technically a form of data that I want persisted across the application, but the ease of use of dispatching events that components were subscribed to seemed to fit this need.
Below is a recipe that I came up with to intercept errors from the server, store them in the store, and have a reusable component subscribe to and display the errors.
Please note: The examples below assume you have a base understanding of the use case of NgRx and that you already have state management setup for your application.
A great reference book that goes deeper into this topic: Architecting Angular Applications with Redux, RxJS, and NgRx
State management
In order for this all to work properly, I am choosing to utilize NgRx to handle the state management of the errors. This allows me to dispatch events to the store and utilize the reducers to hold onto the errors for areas of the application that are subscribed to the store selectors.
Generating a state setup for this purpose is pretty straight forward and so is its corresponding selector:
1 | // form-errors.state.ts |
2 | export interface IFormErrorsState { |
3 | formErrors: any; |
4 | } |
5 | |
6 | export const initialFormErrorsState: IFormErrorsState = { |
7 | formErrors: null, |
8 | }; |
1 | // form-errors.selector.ts |
2 | const selectFormErrorsState = (state: IAppState) => state.formErrors; |
3 | |
4 | export const selectFormErrors = createSelector( |
5 | selectFormErrorsState, |
6 | (state: IFormErrorsState) => state.formErrors |
7 | ); |
The actions
In this case, I need two actions. One to add the errors that are intercepted and a second to clear out the errors. Clearing out the errors is important so that the state of errors does not stay around for other subscribers to pick up. My current application is very single page-esque, so right now there is no sharing of the same component on a given page.
1 | // form-errors.actions.ts |
2 | export enum EFormErrorsActions { |
3 | AddErrors = 'ADD_ERRORS', |
4 | ClearErrors = 'CLEAR_ERRORS', |
5 | } |
6 | |
7 | export class AddErrors implements Action { |
8 | public readonly type = EFormErrorsActions.AddErrors; |
9 | constructor(public payload: ServerErrors[]) { } |
10 | } |
11 | |
12 | export class ClearErrors implements Action { |
13 | public readonly type = EFormErrorsActions.ClearErrors; |
14 | } |
15 | |
16 | export type FormErrorsActions = |
17 | | AddErrors |
18 | | ClearErrors; |
The reducers
Reducing the data received is also pretty simple. We are just storing the data when we get the action to AddErrors
and then subsequently nulling out the state when the ClearErrors
action is received.
1 | // form-errors.reducers.ts |
2 | export const formErrorsReducers = ( |
3 | state = initialFormErrorsState, |
4 | action: FormErrorsActions, |
5 | ): IFormErrorsState => { |
6 | switch (action.type) { |
7 | case EFormErrorsActions.AddErrors: { |
8 | return { |
9 | ...state, |
10 | formErrors: action.payload, |
11 | }; |
12 | } |
13 | case EFormErrorsActions.ClearErrors: { |
14 | return { |
15 | ...state, |
16 | formErrors: null, |
17 | }; |
18 | } |
19 | default: |
20 | return state; |
21 | } |
22 | }; |
Intercepting the errors
Angular makes it simple to setup interceptors and watch responses that come back from a server or even watching for requests. I wrote about one a while ago on adding headers to a request, Angular HTTP interceptors, making development DRY. In this case, I am going to intercept the response for errors that come back.
To do this, we create an interceptor that does one thing, catchError
from the RxJS operator library.
1 | @Injectable() |
2 | export class ErrorInterceptor implements HttpInterceptor { |
3 | constructor( |
4 | private store: Store<IAppState>, |
5 | ) { } |
6 | |
7 | intercept( |
8 | req: HttpRequest<any>, |
9 | next: HttpHandler): Observable<HttpEvent<any>> { |
10 | return next.handle(req).pipe( |
11 | catchError((response: HttpErrorResponse) => { |
12 | if ([401, 422].includes(response.status)) { |
13 | this.store.dispatch(new AddErrors(response.error)); |
14 | } |
15 | return of(response.error); |
16 | }) |
17 | ); |
18 | } |
19 | } |
Currently, there are two error codes I am looking for: 401
and 422
. I could also throw in 500
here, but my current use case is around form submission errors.
When the intercept happens, I call to the store, dispatching the action to add the received errors to the store.
In order to not lock up the application, I make sure to continue on the response of the errors as an observable to the next handler.
Creating the reusable component
Now the final part that glues all of this together and makes things very reusable from form to form; creating a component that observes for these store changes and subsequently displays them.
1 | // server.component.ts |
2 | @Component({ |
3 | selector: 'app-server-errors', |
4 | templateUrl: './server.component.html', |
5 | styleUrls: ['./server.component.scss'] |
6 | }) |
7 | export class ServerErrorsComponent implements OnDestroy { |
8 | public errors$: Observable<any> = |
9 | this.store.select(selectFormErrors); |
10 | |
11 | constructor( |
12 | private store: Store<IAppState>, |
13 | ) { } |
14 | |
15 | ngOnDestroy() { |
16 | this.store.dispatch(new ClearErrors()); |
17 | } |
18 | } |
1 | // server.component.html |
2 | <div *ngIf="errors$ | async as errors"> |
3 | <ul> |
4 | <li *ngFor="let error of errors"> |
5 | { { error } } |
6 | </li> |
7 | </ul> |
8 | </div> |
When this component is created, it starts to observe from the store through the errors selector. This allows the template to display them when it receives them.
The other important part of this component is that it dispatches the ClearErrors
action to the store when the component gets destroyed. This makes sure that other components that may be subscribed to the error state do not display the same errors.
Lots of possibilities
I am finding the use of NgRx super powerful as I add more things to an application I have been working on. With it in conjunction of RxJS, the patterns seem endless in making development very straight forward. I am looking forward to uncovering other use cases to apply the knowledge I have gained in working with Angular, NgRx, and RxJS.