3
votes

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:

  1. I've got form for some kind of entity.
  2. On submit I send POST request to API with just a name of that entity.
  3. In response I get id of newly created entity.
  4. Immediately after that I want to send second request with the rest of form values and with id I've just got.

Where and how should I put that second request?

1

1 Answers

11
votes

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:

  1. 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.
  2. 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, {
                 /* 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.

  • 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([
                    /*
                     * 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.