I'm implementing ngrx in Angular 4 app. Code structure of redux related part is based on example app from ngrx repo (https://github.com/ngrx/example-app). Now I'm wondering how to implement something like that:
Where and how should I put that second request?
How to implement consecutive API calls depends on how cohesive the calls should be.
What I mean by that is whether you view these two calls as a single 'transaction' where both requests have to succeed for you to successfully change your state.
Obviously, if the first request fails, the second request can't be started because it depends on data from the first request. But...
What should happen when the first request succeeds and the second request fails?
Can your app continue it's work with only the id
from the first request and without the second request, or will it end up in an inconsistent state?
I am going to cover two scenarios:
Since both requests have to succeed you can view both requests as if they were only one request. In this case I suggest to hide the consecutive calls inside a service (this approach is not specific to ngrx/redux, it is just plain RxJs):
@Injectable()
export class PostService {
private API_URL1 = 'http://your.api.com/resource1';
private API_URL2 = 'http://your.api.com/resource2';
constructor(private http: Http) { }
postCombined(formValues: { name: string, age: number }): Observable<any> {
return this.http.post(this.API_URL1, { name: formValues.name })
.map(res => res.json())
.switchMap(post1result =>
this.http.post(this.API_URL2, {
/* access to post1result and formValues */
id: post1result.id,
age: formValues.age,
timestamp: new Date()
})
.map(res => res.json())
.mergeMap(post2result => Observable.of({
/* access to post1result and post2result */
id: post1result.id,
name: post1result.name,
age: post2result.age,
timestamp: post2result.timestamp
})
);
}
}
Now you can use the postCombined
-method in an effect like any other service-method as showcased in the ngrx-example-app.
mergeMap
. As you can see, it is possible to return merged data from both request-responses.With this approach you can distinguish the result of the two requests and react differently if either one fails. I suggest to break the two calls into independent actions so you can reduce each case independently.
First, the service now has two independent methods (nothing special here):
post.service.ts
@Injectable()
export class PostService {
private API_URL1 = 'http://your.api.com/resource1';
private API_URL2 = 'http://your.api.com/resource2';
constructor(private http: Http) { }
post1(formValues: { name: string }): Observable<{ id: number }> {
return this.http.post(this.API_URL1, formValues).map(res => res.json());
}
post2(receivedId: number, formValues: { age: number }): Observable<any> {
return this.http.post(this.API_URL2, {
id: receivedId,
age: formValues.age,
timestamp: new Date()
})
.map(res => res.json());
}
}
Next define request-, success- and failure-actions for both requests:
post.actions.ts
import { Action } from '@ngrx/store';
export const POST1_REQUEST = 'POST1_REQUEST';
export const POST1_SUCCESS = 'POST1_SUCCESS';
export const POST1_FAILURE = 'POST1_FAILURE';
export const POST2_REQUEST = 'POST2_REQUEST';
export const POST2_SUCCESS = 'POST2_SUCCESS';
export const POST2_FAILURE = 'POST2_FAILURE';
export class Post1RequestAction implements Action {
readonly type = POST1_REQUEST;
constructor(public payload: { name: string, age: number }) { }
}
export class Post1SuccessAction implements Action {
readonly type = POST1_SUCCESS;
constructor(public payload: { id: number }) { }
}
export class Post1FailureAction implements Action {
readonly type = POST1_FAILURE;
constructor(public error: any) { }
}
export class Post2RequestAction implements Action {
readonly type = POST2_REQUEST;
constructor(public payload: { id: number, name: string, age: number}) { }
}
export class Post2SuccessAction implements Action {
readonly type = POST2_SUCCESS;
constructor(public payload: any) { }
}
export class Post2FailureAction implements Action {
readonly type = POST2_FAILURE;
constructor(public error: any) { }
}
export type Actions
= Post1RequestAction
| Post1SuccessAction
| Post1FailureAction
| Post2RequestAction
| Post2SuccessAction
| Post2FailureAction
And now we can define two effects that will run when the request-actions are dispatched and in turn will dispatch either success- or failure-actions dependent on the outcome of the service-call:
post.effects.ts
import { PostService } from '../services/post.service';
import * as post from '../actions/post';
@Injectable()
export class PostEffects {
@Effect()
post1$: Observable<Action> = this.actions$
.ofType(post.POST1_REQUEST)
.map(toPayload)
.switchMap(formValues => this.postService.post1(formValues)
.mergeMap(post1Result =>
Observable.from([
/*
* dispatch an action that signals that
* the first request was successful
*/
new post.Post1SuccessAction(post1Result),
/*
* dispatch an action that triggers the second effect
* as payload we deliver the id we received from the first call
* and any other values the second request needs
*/
new post.Post2RequestAction({
id: post1Result.id,
name: formValues.name,
age: formValues.age
})
])
)
.catch(err => Observable.of(new post.Post1FailureAction(err)))
);
@Effect()
post2$: Observable<Action> = this.actions$
/*
* this effect will only run if the first was successful
* since it depends on the id being returned from the first request
*/
.ofType(post.POST2_REQUEST)
.map(toPayload)
.switchMap(formValuesAndId =>
this.postService.post2(
/* we have access to the id of the first request */
formValuesAndId.id,
/* the rest of the form values we need for the second request */
{ age: formValuesAndId.age }
)
.map(post2Result => new post.Post2SuccessAction(post2Result))
.catch(err => Observable.of(new post.Post2FailureAction(err)))
);
constructor(private actions$: Actions, private postService: PostService) { }
}
Notice the mergeMap
in combination with Observable.from([..])
in the first effect. It allows you to dispatch a Post1SuccessAction
that can be reduced (by a reducer) as well as a Post2RequestAction
that will trigger the second effect to run. In case the first request fails, the second request will not run, since the Post2RequestAction
is not dispatched.
As you can see, setting up actions and effects this way allows you to react to a failed request independently from the other request.
To start the first request, all you have to do is dispatch a Post1RequestAction
when you submit the form. Like this.store.dispatch(new post.Post1RequestAction({ name: 'Bob', age: 45 }))
for example.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With