Pull and Push-based mechanisms in Angular

2022-09-107 min
Pull and Push-based mechanisms in Angular

Introduction

Dear visitor, Before We dig into the main topic of the article I want to share something with you. At the beginning of my journey with Angular, I quickly noticed that there are some concepts that seemed to be more complex than others. The most difficult was a Reactive Programming paradigm. At that time, it was something new to me. The most challenging part was to change my mindset from Imperative programming to Declarative one.

RxJS

In Angular We can use built-in library - RxJS.

RxJS is a powerful library, offering functional, declarative approach for dealing with asynchronicity.

But still, having such a powerful tool doesn't mean that We always take full advantage of it. And that's how the story goes with Pull and Push-based mechanisms.

So what is the meaning of Pull and Push-based mechanisms and how to distinguish them?

Observer Pattern

First of all, those mechanisms are associated with Observer Pattern. Observer Pattern is a behavioral design pattern, which means that it defines the comunication between objects. It is designed to be one-to-many relationship, so if one object(Subject) is modified, its depenedent objects(Observers) are to be notified automatically. You can see below on attached diagram.

Observer pattern

The important part here is the communication mechanism between Observer and Subject, it can be Pull or Push.

So how do We know which of those two mechanisms We deal with?

Differences between Pull and Push.

Let's ask following question: Who decides about the emergence of new data in our component? If it's the component(Observer) itself(by calling fetch methods), then it's a Pull, but if the data comes to the component from the outside, so someone else(Subject) decides about it, then it's a Push.

  • The fact is that component inputs are also Push-based, but let's not focus on it at all.

Pull mechanism in practice

As an example let's consider following situation. We want to render the view with an articles list inside. To do that We need articles data. Let's see how our implementation may look like:

articles.component.ts

articles: Article[] = [];

ngOnInit() {
  this.articleService.getArticles().subscribe(articles => this.articles = articles);
}

What We do here is We call articleService to fetch/pull the articles in the OnInit hook. Simply speaking, our component(Observer) decides to Pull the data, which means that it's a Pull-based mechanism. Let's add a bit more to this example to make it a bit more complex.

articles.component.ts

articles: Article[] = [];
searchControl = new FormControl();

constructor(private activatedRoute: ActivatedRoute) {}

ngOnInit() {
  const { search } = this.activatedRoute.snapshot.queryParams;
  this.searchControl.setValue(search || '');

  this.searchControl.valueChanges
    .pipe(//transforms and unsubscribe stuff)
    .subscribe(search => this.router.navigate(['articles'], { queryParams: { search } }));
    
   this.activatedRoute.queryParams
     .pipe(
     .   // unsubscribe stuff
        .switchMap(params => this.articleService.getArticles(params['search'] || ''))
     )
     .subscribe(articles => this.articles = articles);
}

articles.component.html

<ul>
  <li *ngFor="let article of articles">{{ article.title }}</li>
</ul>
<input type="seaarch" [formControl]="searchControl" />

In above code We added search feature. Right now We have a lot of going on in ngOnInit hook:

  • We subscribe to each search criteria change and update queryParams, in order to have search in the URL.
  • subscribe to queryParams change, and based on its value pull the articles from articlesService.
  • set initial searchControl value based on the state snapshot of queryParams on init,

Summing up, component(Observer) still decides WHEN to Pull the data.

I think that most of us is familiar with this approach and have done it many times, so let's dive into pros and cons.

Pros

  • low entry threshold,
  • easy to use, especially in small, not complex apps,
  • more flexible, component pull the data by itself so it doesn't rely on data provider,

Cons

  • have to remember of unsubscribing, f.ex control.valueChanges in above example,
  • increased complexity, especially when multiple views requires the same data,
  • part of the logic is in the component, when it should be in the service,
  • slower due to change detection cycle,
  • component needs informations such as (data, params, context) in order to pull the right information,
  • code might be more coupled,

Okay, now We know what is Pull-based mechanism, but let's bring the balance to the galaxy!

Time to Push!

We already know the theory of Push-based meshanism, so let's jump straight into the example! In order to better see the differences I will implement the same functionality.

articles.service.ts

@Injectable({ providedIn: 'root' })
export class ArticlesService {
  search$ = new BehaviorSubject<string>('');
  qParams$ = this.activatedRoute.queryParams;
  initialQParams$ = this.qParams$.pipe(take(1));
  articles$ = this.qParams$.pipe(
    map(params => params['search']),
    switchMap(search => this.getArticles(search)),
    shareReplay(1),
  );

  constructor(
    private activatedRoute: ActivatedRoute,
    private http: HttpClient,
  ) {
     this.search$.subscribe(search => this.router.navigate(['articles'], { queryParams: { search } }));
     this.initialQParams$.subscribe(params => this.setSearch(params['search']));
  }
  
  getSeach$(): Observable<string> {
    return this.search$.asObservable();
  }

  setSearch(value: string): void {
    this.search$.next(value || '');
  }

  getArticles(search: string): Observable<Article[]> {
    return this.http.get<Article[]>(`${API_URL}/articles`, {
      params: {
        search
      },
    });
  }
}

articles.component.ts

articles$ = this.articlesService.articles$;
search$ = this.articlesService.getSeach$();

constructor(private articlesService: ArticlesService) {}

onSearch(search: string): void {
  this.articlesService.setSearch(search);
}

articles.component.html

<input type="search" [ngModel]="search$ | async" (ngModelChange)="onSearch($event)" />

<div *ngFor="let article of articles$ | async">
  {{ article.name }}
</div>

And this is how the implementation looks like. Let's list what's going on here:

  • logic is moved to articles.service.ts,
  • everything is a stream of data - Observable or Subject,
  • no subscribe in component, instead usage of async pipe - We don't need to unsubscribe at all,
  • component(Observer) doesn't decide about emergence of new data,
  • component simply subscribes to an articles$ and gets notified on every data change,

Let's talk about pros and cons of this solution.

Pros:

  • no coupling in case multiple views require the same data,
  • more scalable,
  • don't need to remember about unsubscribe,
  • component is free of logic about HOW and WHEN to provide the data,
  • better performance, getting new data doesn't rely on change detection,
  • easier to use with onPush change detection strategy,

Cons

  • brings unnecessary complexity in small apps, especially when there is only one Observer,
  • less flexible,
  • higher entry threshold,

Conclusion

Before We decide to use Pull or Push mechanisms, We need to consider if this is the right tool for the job. Now, when We know both mechanisms and its pros and cons, it should be much easier.

Hope you enjoy!