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:
- Scenario 1: When either of the requests fail you treat it as the whole 'transaction' has failed and therefore don't care which request has failed.
- Scenario 2: When request 1 fails, request 2 will not be executed. When request 2 fails, request 1 will still be viewed as successful.
Scenario 1
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, {
id: post1result.id,
age: formValues.age,
timestamp: new Date()
})
.map(res => res.json())
.mergeMap(post2result => Observable.of({
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.
- If either request fails, the service will throw an error which you can catch and handle in your effect.
- If both requests succeed, you will get back the data that is defined inside of
mergeMap
. As you can see, it is possible to return merged data from both request-responses.
Scenario 2
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([
new post.Post1SuccessAction(post1Result),
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$
.ofType(post.POST2_REQUEST)
.map(toPayload)
.switchMap(formValuesAndId =>
this.postService.post2(
formValuesAndId.id,
{ 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.