Simple Reactive Angular Component Architecture Question

Hey all, here’s an open question to try and get insight into how others approach structuring reactive components in Angular. Feel free to plug your own articles or content along with your answer.

Given the following component (or see stackblitz: https://stackblitz.com/edit/angular-qfwndd)

The accumulator$ misses the emissions from the subject in ngOnInit because async pipe has not yet been created / subscribed to accumulator$.

How would you refactor this component? Simply move the logic to afterViewInit? Extract the logic to somewhere else? Etc…

Below is a contrived example, this could be with HTTP calls or any other type of event.

@Component({
  selector: "app-basic",
  template: `
    <div *ngIf="accumulator$ | async as data">
      {{data | json}}
    </div>
  `,
  styles: []
})
export class BasicComponent implements OnInit {

  subject = new Subject();
  accumulator$ = this.subject.asObservable().pipe(
    scan((acc, curr) => Object.assign({}, acc, curr), {})
  );

  ngOnInit(): void {
    // With async pipe these emissions are missed
    this.subject.next({ name: "Erik" });
    this.subject.next({ age: 25 });
    this.subject.next({ language: "Javascript" });
  }
}

1 Like

Hey,

I would look at using ngChanges, but check within that if the values have changed. NgChanges runs on all value changes, so you might need to check if the values are not null.

Check the docs for how to you it, see if it works for you.

Stephen

1 Like

You say

this could be with HTTP calls (…)

but HTTP requests are resolved asynchronously like most other events and data sources, meaning the values wouldn’t be available during ngOnInit anyways.

Exactly :slight_smile:

Given any arbitrary event/data-source, which is unknown whether or not it will be available in ngOnInit, how do you handle aggregating/displaying/calculating some form of state when a component is created. – There are many ways to handle this, but how do you like to structure your application/components to solve this problem/situation?

This may result in a really long answer – references to article or videos that address this sort of thing instead would be great as well. Sorry if my question is too ambiguous or doesn’t make sense.

Would NgRx Store + Effects be a sufficient answer? :smile:

1 Like

Haha definitely. – Do you have any articles or videos on NgRx Store + Effects you could recommend?

1 Like

The NGRX Store + Effects course by Todd Motto was my first introduction and it made sense to me. Todd recently updated it for NgRx v8.

1 Like

Here’s one way to solve the late subscriber problem. Connect an observable immediately to make it hot.

// basic.component.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ConnectableObservable, Subject } from 'rxjs';
import { publishReplay, scan } from 'rxjs/operators';

@Component({
  selector: 'app-basic',
  styles: [':host { display: block; }'],
  template: `
    <div *ngIf="state$ | async as state">
      {{state | json}}
    </div>
  `,
})
export class BasicComponent implements OnDestroy, OnInit {
  stateEvent = new Subject<{ [key: string]: number | string }>();
  state$ = this.stateEvent.pipe(
    scan((state, event) => Object.assign({}, state, event), {}),
    publishReplay(1),
  ) as ConnectableObservable<any>;

  ngOnInit(): void {
    this.state$.connect();
    this.stateEvent.next({ name: "Erik" });
    this.stateEvent.next({ age: 25 });
    this.stateEvent.next({ language: "Javascript" });
  }

  ngOnDestroy(): void {
    this.stateEvent.complete();
  }
}

Here, I’ve split it into a container component and a presentational component:

// advanced.container.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ConnectableObservable, Subject } from 'rxjs';
import { publishReplay, scan } from 'rxjs/operators';

import { AdvancedUiScam } from './advanced.component';

@Component({
  selector: 'app-advanced',
  styles: [':host { display: block; }'],
  template: `
    <app-advanced-ui [state]="state$ | async"></app-advanced-ui>
  `,
})
export class AdvancedContainerComponent implements OnInit {
  private stateEvent = new Subject<{ [key: string]: number | string }>();

  state$ = this.stateEvent.pipe(
    scan((state, event) => Object.assign({}, state, event), {}),
    publishReplay(1),
  ) as ConnectableObservable<{ [key: string]: number | string }>;

  ngOnInit(): void {
    this.state$.connect();
    this.stateEvent.next({ name: "Erik" });
    this.stateEvent.next({ age: 25 });
    this.stateEvent.next({ language: "Javascript" });
  }

  ngOnDestroy(): void {
    this.stateEvent.complete();
  }
}
// advanced.component.ts
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'app-advanced-ui',
  styles: [':host { display: block; }'],
  template: `
    <div>
      {{ state | json }}
    </div>
  `,
})
export class AdvancedComponent {
  @Input()
  state: { [key: string]: number | string };
}

They’re all here in this StackBlitz workspace:

Special thanks to @Michael_Hladky for a sneak preview of his article on this topic :smiley:

2 Likes

I can also recommend the new library RxAngular State which very carefully takes technical RxJS details like these into account:

1 Like

@LayZee – Thanks Lars! I appreciate the detailed info and references! :+1:

1 Like

Hi @erxk_verduin , can you try using ReplaySubject instead of Subject?

I haven’t tried it myself, but it seems to me that if you will use ReplaySubject with a big count to be safe, lets say a 1000, it should just work.

So, try to replace this line:
subject = new Subject();
with this line:
subject = new ReplaySubject(1000);

1 Like

Zohar, good suggestion, thanks!

This is essentially what Lars does in his example above by using “publishReplay(1)”

publishReplay is a shortcut for: multicast(() => new ReplaySubject(x))

I connect the connectable observable immediately. Slighty different, but important distinction that makes it hot.

1 Like