RxJS in Angular – Antipattern 2 – Stateful Streams

This is the second part of a small series, in which I want to share some pitfalls we discovered multiple times in code reviews over the years, as well a few patterns we identified as helpful. In the first part we discussed how nesting subscriptions can be harmful.

In diesem Artikel:

RxJS in Angular – Antipattern 2 – Stateful Streams
Yannick Baron ist Architekturberater bei Thinktecture mit einem Fokus auf Angular und RxJS.

Stateful Streams in Use

Often when modeling our streams with RxJS, we end up in a situation where we need to switch our streams but need to keep their results together. Naturally, the need to store the intermediate result emerges. A common attempt to solve this problem we encountered numerous times would be to store these intermediate results outside of our stream. In the case of using Angular, the destination often tends to be the component.

				
					this.route.params.pipe(
  switchMap(({ customerId }) => customerService.getCustomer(customerId)),
  tap((customer) => { 
    this.customer = customer;
    this.code = makeCode(customer);
  }),
  switchMap(() => myService.retrieveByCode(this.code)),
  tap((result) => { this.result = result; }),
  switchMap(() => otherService.byCustomerAndResult(this.customer, this.result)),
).subscribe(combinedResult => {
  this.result = combinedResult;
  this.view = moreComplexComutation(this.customer, this.code, combinedresult);
});

				
			

In the above implementation we react to the change of a route param customerId. Once it changes, we switchMap to request an customer object from our API. In the following tap, we store the intermediate results on our component. We then switchMap to hit our API again using the previously stored this.code and use the following tap to once again store this intermediate result on our component. The next switchMap also requests further information from our API and finally all intermediate results are combined and the view is updated in our subscribe.

The Bad

At every step of the pipeline we are relying on state that is kept outside of our stream. This can introduce side-effects and we cannot argue about our data flow easily, as we cannot know at what point in time which property will be updated, thus making our stream rely on state.

Imagine there is some effect or action outside of the stream mutating these values. It is not obvious to the developer looking at the code that there might actually be something tampering with the state while the stream is executing.

What if one of the requests fails? Then our state will go out of sync and maybe even produce erroneous results. In order to prevent this, proper error handling becomes a hassle, as we have to handle every request separately and perform the appropriate cleanup.

The Fix

The need for state arose from the need to store intermediate results that can be picked up at a later stage of our pipeline. We now want to introduce a way to model our streams that alleviates the need to store results outside of the stream and rather carry them along through our pipeline.

				
					// Emitting customerIds 1 and 2 with a 25ms delay
createStream<number>([1, 2], 25)
  .pipe(
    // Switch to request customer object
    switchMap(id => requestCustomer(id)),
    // Switch to request made by generating the customer code
    switchMap((customer) => {
      const code = makeCode(customer);

      return requestByCode(code)
        // Combine results into a bundle
        .pipe(map(result => ({ customer, code, result })));
    }),
    // Switch to final request
    switchMap(({ customer, result, ...bundle }) => {
      return requestByCustomerAndResult(customer, result)
        // Add the new result to the bundle
        .pipe(map(combinedResult => ({ ...bundle, customer, result, combinedResult })));
    }),
  )
  .subscribe(({ customer, code, result, combinedResult }) => {
    // Update view or properties all at once
    updateView('customer', customer.name);
    updateView('code', code);
    updateView('result', result.result);
    updateView('combined', combinedResult);
  });

				
			

In the snippet above, we react to our stream emitting customer ids. We then switchMap to request the corresponding customer object. Once retrieved, we compute the code and switchMap to request more data from our API. At this point we use map to transform the response to a bundle containing the customer, our code and the result of our request. From this bundle, we use what we need (customer and result) to finally switchMap to our final request. Once retrieved, we once again combine all of our results into a bundle. Finally, we have all the data we need to update our view accordingly in a single step, making sure nothing can go out of sync.

Please note how the use of destructuring helps us accessing the parts of our bundles easily.

You can find a running example on stackblitz.

Conclusion

In this post we have discussed how keeping state outside of our stream can potentially introduce unforeseen side-effects. We introduced a simple way to remove the need for storing intermediate results, which keeps our data flow simple and once again makes it easier to argue about how our data flows. This also increases the readability for fellow developers as they just have to follow the pipeline and not worry about a state that could potentially be manipulated from outside of the stream.

In the next part of the series we show how employing the techniques introduced can make working with RxJS in Angular applications easier for us.

Kostenloser
Newsletter

Aktuelle Artikel, Screencasts, Webinare und Interviews unserer Experten für Sie

Verpassen Sie keine Inhalte zu Angular, .NET Core, Blazor, Azure und Kubernetes und melden Sie sich zu unserem kostenlosen monatlichen Dev-Newsletter an.

Diese Artikel könnten Sie interessieren
Angular
Configuring Lazy Loaded Angular Modules

Configuring Lazy Loaded Angular Modules

Making our Angular modules configurable is an important step in building a reusable architecture. Having used Angular for a while you might be familiar with the commonly used forRoot() and forChild() functions, that some modules provide you with. But what is the best way to provide configuration in these cases?
16.06.2022
Angular
Master Web Component Forms Integration – with Lit and Angular

Master Web Component Forms Integration – with Lit and Angular

When a company has cross-framework teams, it is a good choice to use Web Components to build a unified and framework-independent component library. However, some pitfalls are to consider when integrating these components into web forms. Therefore, for a better understanding, we will look at two possible approaches and try to integrate them into an Angular form as an example.

Notice: All code samples are available on Github!
09.06.2022
.NET
Asynchrone Operationen: Blazor WebAssembly für Angular-Entwickler – Teil 5 [Screencast]

Asynchrone Operationen: Blazor WebAssembly für Angular-Entwickler – Teil 5 [Screencast]

Eine Webanwendung will natürlich auch mit Daten gefüttert werden. Doch diese müssen irgendwo her kommen. Nichts liegt näher als diese von einer Web API zu laden. Dieser Screencast zeigt, wie asynchrone Operationen in Blazor funktionieren und welche gravierenden Unterschiede es zu Angular gibt.
26.05.2022
.NET
Typings: Blazor WebAssembly für Angular-Entwickler – Teil 4 [Screencast]

Typings: Blazor WebAssembly für Angular-Entwickler – Teil 4 [Screencast]

C# und TypeScript entstammen der Feder der selben Person. Doch sind sie deshalb auch gleich? In diesem Teil der Screencast-Serie erfahren Sie, wie mit Typen in den beiden Programmiersprachen verfahren wird und welche Unterschiede es gibt.
19.05.2022
.NET
Bindings: Blazor WebAssembly für Angular-Entwickler – Teil 3 [Screencast]

Bindings: Blazor WebAssembly für Angular-Entwickler – Teil 3 [Screencast]

Wer Komponenten einsetzt, steht früher oder später vor der Fragestellung, wie man Daten an die Komponente übergibt oder auf Ereignisse einer Komponente reagiert. In diesem Screencast wird gezeigt wie Bindings bei Komponenten funktionieren, also wie eine Komponente Daten von außerhalb benutzen und Rückmeldung bei Aktionen geben kann.
12.05.2022
.NET
Komponenten: Blazor WebAssembly für Angular-Entwickler – Teil 2 [Screencast]

Komponenten: Blazor WebAssembly für Angular-Entwickler – Teil 2 [Screencast]

Bei der Entwicklung einer Webapplikation kommt es ständig vor, dass UI-Teile immer und immer wieder verwendet werden. Damit nicht immer Copy & Paste verwendet werden muss, können diese Teile in Komponenten zusammengefasst werden.
05.05.2022