Matt Hintzke
Matt Hintzke

Reputation: 7994

How to perform concurrent HTTP requests on rows of a table

I am having a hard time working with observables and a distributed API. I have a table with many rows and for each row I need to go out and fetch a "status" for the row using a seperate API on a seperate domain service. The obvious solution would be to load everything upfront like this:

I have 10 rows so I store 10 models in an array like myArray:Model[]. I could then subscribe to a HTTP request to fetch these models and store them on my component in this array. After that request gets back I could then iterate through each Model and perform another request to get "status" of each model. This would require me to either 1) Convolute the Model domain model class with a Status property and then do another .subscribe on the status call which would then allow me to set the Status property for each. Now I can finally render the table as a whole. OR 2) I could have a second array like myStatuses:StatusModel[] which contains all of the statuses for all of my Models and then in the table do a getStatusForModel(model) which would have to find the right status at runtime.

This feels like a TON of code for doing something fairly simple.

I would have thought I could not add all this extra state by using observables and the async pipes... however, when I try to use an async pipe with a call like getStatusAsync(model) that returns a Promise<StatusModel> my browser goes into an infinite loop because its returning a Promise each time and the change detection is forcing the view to update over and over again.

Basically I cannot figure out what is the intended use for performing async operations without having to hold so much state within my components. What is the point of the AsyncPipe if I can't even return an actual Promise and only have the returned value be tracked by change detection and not the Promise object itself?

I feel like Observables and async in Angular is adding so much more code and complexity than it should. Am I doing something wrong or is this just the reality of using observables and promises now?

Upvotes: 0

Views: 460

Answers (1)

meriton
meriton

Reputation: 70564

My sympathies for having to work with such a cumbersome API.

Yes, "unwrapping" the Observable is easiest done with the async pipe if you only need it in the template.

Yes, the async pipe will subscribe anew whenever it receives a different Observable instance. That's because angular has no way of knowing that the new observable is equivalent to the old one.

As an aside, invoking functions in change detected expressions is generally frowned upon because the function is executed during every change detection run (yes, even if the arguments have not changed; angular has no way of knowing the return value only depends on the arguments).

Therefore, you should not create the status observable during change detection.

That is, I'd do something like

models = this.fetchModels().pipe(map(
  models => models.map(
    model => {...model, status: this.fetchStatus(model)}
  )
));

And in the template

<tr *ngFor="let model of models | async">
  <td>{{model.name}}</td>
  <td>{{model.status | async}}</td>
</tr>

Note that this solution will cause new API requests to be emitted whenever a model becomes visible in the table. This may be more often than you want. You can control how often status is reloaded by using appropriate rxjs operators, or by unwrapping the status observable by manually subscribing to it in your typescript code.

Appendix: Isn't this violating DDD?

You asked:

The reason I didn't want to do this is in proper DDD you should have domain objects that are decoupled from one another.

I don't think it is a clear cut as that: DDD repositories return aggregates, not individual entities.

In true SOA, I would have an Order API on one service and a Invoice API on a second service. I would then fetch my list of Orders from the Order API and then subsequently make a request to get the Invoice for each Order. Generally, I thought it was bad to mix models on the UI - meaning I wouldn't have to force a Order.InvoiceStatus property when the backend models do not do this at all. This is coupling domain models.

In DDD, there is a ubiquitous language within every bounded context.

You seem to consider your UI and backend to be within the same bounded context, but if so, why are they using structurally different models? Why is the invoice status displayed in the order table, but stored in an invoice table?

In DDD, such tension between ui and backend about the structure of your domain can be resolved in two ways:

  • Shared Kernel: UI and backend agree on a common model. The model is changed so it suits everyone. In your case, that would mean that if the UI needs the invoice status on the order, the backend should return the invoice status on the order.

  • Anti Corruption Layer: UI and backend have different models, and models are transformed as they pass through the boundary. That is, the domain model would be converted into a view model more suitable for the UI during API access.

You will have to decide which of these approaches fits your application better.

I was hoping I could keep these seperated, but it sounds like the way Angualr would prefer this to work is to introduce a 3rd model like OrderViewModel that has a handle on an Order as well as an Invoice (and possibly other models). Does that sound correct to you?

Technically, angular does not care how your structure your data model. It can databind to pretty much anything.

But from a best practice point of view, I find it important that each tier can work with a model that suits it. So yes, if you can't use a shared kernel, I would recommend a (lightweight) anti corruption layer.

Upvotes: 2

Related Questions