This Banner is For Sale !!
Get your ad here for a week in 20$ only and get upto 15k traffic Daily!!!

Async pipe is not pure 🤯


Sure, that’s regular! Why? Due to how Angular does change detection underneath the hood!

To higher perceive how the async pipe works, let’s create our personal from scratch!

Let’s name it SubscribePipe to ensure that to vary it from the original one.

Additionally, we wish our pipe to verify this factors:

  • Work with Observables (we received’t deal with Guarantees on this submit)
  • No over-subscribing
  • Work with OnPush change detection
  • No reminiscence leaks

And use it like this, ex:

@Part({
  selector: 'my-app',
  template: `
    <div *ngIf="present">
      Worth: { subscribe }
    </div>
  `,
  standalone: true,
  imports: [CommonModule, SubscribePipe],
  // changeDetection: ChangeDetectionStrategy.OnPush // <-- after we deal with it we should always uncomment this
})
export class AppComponent {
  present = true;

  obs$ = interval(500).pipe(
    faucet((x) => {
      if (x === 10) {
        this.present = false;
      }
    })
  );
}
Enter fullscreen mode

Exit fullscreen mode



Create a pipe (standalone)

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  identify: 'subscribe',
  standalone: true
})
export class SubscribePipe implements PipeTransform {
  remodel() {}
}
Enter fullscreen mode

Exit fullscreen mode

We wish our pipe to simply accept an Observable sort, meaning we have to deal with Observables, Topics, BehaviorSubjects and ReplaySubjects.

Let’s create a Subscribable sort that features all of them (that will likely be a generic sort).

sort Subscribable<T> = Observable<T> | Topic<T> | BehaviorSubject<T> | ReplaySubject<T>;
Enter fullscreen mode

Exit fullscreen mode

Now that we all know what our pipe accepts, let’s refactor the pipe to make use of it! As a result of we need to infer the kind of the subscribable we are going to convert the pipe class to be generic.

Additionally, one different factor, we are able to additionally move undefined or null to our pipe.

export class SubscribePipe<T> implements PipeTransform {
  remodel(obs: Subscribable<T> | null | undefined) {}
}
Enter fullscreen mode

Exit fullscreen mode



Subscription dealing with

First we have to verify if the observable will not be null or undefined, and whether it is we simply return null;

remodel(obs: Subscribable<T> | null): T | null {
  if (!obs) {
    return null;
  }
}
Enter fullscreen mode

Exit fullscreen mode

Now, let’s subscribe to the observable and retailer it’s final worth and return it immediately.

latestValue: T | null = null;

remodel(obs: Subscribable<T> | null): T | null {
  if (!obs) {
    return null;
  }

  obs.subscribe(worth => {
    this.latestValue = worth;
  });

  return this.latestValue;
}
Enter fullscreen mode

Exit fullscreen mode

This received’t work! Why?

As a result of when change detection runs, the pipe will verify the parameters within the remodel technique and in the event that they haven’t modified, it should return the final worth that it had cached earlier than.

I’ve defined it extra deeply in my earlier submit: It’s okay to make use of operate calls in Angular templates! , the place I clarify how pipes memoization works and the way we are able to do the identical factor too when utilizing regular capabilities.

That is the second the place we opt-out of pipe memoization by utilizing the pure: false flag.

@Pipe({
  identify: 'subscribe',
  standalone: true,
  pure: false // <-- It's true by default
})
Enter fullscreen mode

Exit fullscreen mode

The second we set the pure choice to be false, we inform Angular that we need to deal with the remodel technique memoization by ourself.

If we run the code we are going to see one thing like this:

Not working solution

The rationale why it does that’s as a result of each time change detection runs, the pipe will subscribe to our observable and let the opposite subscription in reminiscence, and naturally create a reminiscence leak!

How can we repair that? By doing a easy equality verify!

We’ll save the present observable reference, and each time the remodel technique is named, we are going to verify if it’s equal with our present one, and if that’s true, we are going to simply return that newest worth.

non-public currentObs: Subscribable<T> | null = null;

remodel(obs: Subscribable<T> | null): T | null {
  if (!obs) {
    return null;
  }

  if (obs === this.currentObs) { // <-- easy equality verify
    return this.latestValue;
  } else {
    this.currentObs = obs; // <-- save present observable to a category area

    obs.subscribe((worth) => {
      this.latestValue = worth;
    });
  }

  return this.latestValue;
}
Enter fullscreen mode

Exit fullscreen mode

If we verify the app now, we are going to see that it really works positive! However it’s not completed!

Working solution

As a result of we nonetheless trigger a reminiscence leak, as a result of we by no means unsubscribe from the observable! Let’s do it!



Unsubscription dealing with

Similar to we saved the latestValue and currentObs, we may also retailer the present subscription and assign the observable subscription to it!

non-public sub: Subscription | null = null;

remodel(obs: Subscribable<T> | null): T | null {
  ...
  this.sub = obs.subscribe((worth) => {
    this.latestValue = worth;
  });
  ...
}
Enter fullscreen mode

Exit fullscreen mode

Good! Now we have to unsubscribe on ngOnDestroy, however not solely there 💡. How so? As a result of we have to unsubscribe additionally on instances the place we change the observable reference to be one other observable or set it to null.

For instance:

@Part({
  selector: 'my-app',
  template: `
    <div *ngIf="present">{ subscribe }</div>
  `,
  standalone: true,
  imports: [CommonModule, SubscribePipe],
})
export class AppComponent {
  present = true;

  ngOnInit() {
    setTimeout(() => {
      this.obs$ = of(20000);
    }, 2000);

    setTimeout(() => {
      this.obs$ = null;
    }, 4000);
  }

  obs$ = interval(500).pipe(
    faucet((x) => {
      if (x === 10) {
        this.present = false;
      }
    })
  );
}
Enter fullscreen mode

Exit fullscreen mode

So, we have to dispose the subscription additionally on these instances! In any other case, we trigger a reminiscence leak!

Let’s create a dispose() technique that has the unsubscription logic with a purpose to re-use it!

non-public dispose() {
  if (this.sub) { // <-- first we verify if we've got a subscription
    this.sub.unsubscribe(); // <-- unsubscribe from the observable
    this.sub = null; // <-- take away the subscription reference
  }
}
Enter fullscreen mode

Exit fullscreen mode

Now, let’s use this technique!

First, on ngOnDestroy() after which within the different instances talked about above!

ngOnDestroy() {
  this.dispose();
}
Enter fullscreen mode

Exit fullscreen mode

remodel(obs: Subscribable<T> | null): T | null {
  if (!obs) {
    this.dispose(); // <-- if we've got a present sub and alter the obs to be null we have to dispose it
    return null;
  }

  if (obs === this.currentObs) {
    return this.latestValue;
  } else {
    this.dispose(); // <-- earlier than subscribing to a brand new observable, we have to dispose the prevailing one

    this.currentObs = obs;

    this.sub = obs.subscribe((worth) => {
      this.latestValue = worth;
    });
  }

  return this.latestValue;
}
Enter fullscreen mode

Exit fullscreen mode

If we see the app, it should nonetheless work with none difficulty! And now with out reminiscence leaks 🎉 yay!

Are we accomplished? Not but, as a result of our pipe doesn’t work with OnPush ChangeDetection!

On, your editor, attempt to allow the changeDetection OnPush in your element and see the app! It received’t present something!

However, how can we repair it? Similar to another time once we attempt to repair change detection points 😈😄, put a cdr.markForCheck() after you replace the worth and we’re accomplished!

non-public cdr = inject(ChangeDetectorRef); // <-- inject CDRef right here

remodel(obs: Subscribable<T> | null): T | null {
  ...
  this.sub = obs.subscribe((worth) => {
    this.latestValue = worth;
    this.cdr.markForCheck(); // <-- mark the element as soiled right here, after we've got up to date the latestValue
  });
  ...
}
Enter fullscreen mode

Exit fullscreen mode

That’s it!



Code refactoring

Let’s transfer the subscription dealing with in a technique (and likewise throw once we get an error), set cdr, currentObs to null in ngOnDestroy (to take away it’s reference and never trigger any reminiscence leak) and alter the best way code is dealt with within the remodel technique for higher readability.

non-public subscribe(obs: Subscribable<T>) {
  this.currentObs = obs;

  this.sub = obs.subscribe({
    subsequent: (res) => {
      this.latestValue = res;
      this.cdr.markForCheck();
    },
    error: (error) => {
      throw error;
    },
  });
}
Enter fullscreen mode

Exit fullscreen mode

remodel(obs: Subscribable<T> | null): T | null {
  if (!obs) {
    this.dispose();
    return null;
  }

  // right here we verify if the obs aren't the identical as an alternative of checking if they're the identical
  if (obs !== this.currentObs) {
    this.dispose();
    this.subscribe(obs); // <-- use the strategy we extracted above
  }

  return this.latestValue;
}
Enter fullscreen mode

Exit fullscreen mode

ngOnDestroy() {
  this.dispose();
  this.cdr = null;
  this.currentObs = null;
}
Enter fullscreen mode

Exit fullscreen mode



Recap

Discover the entire supply code here.

We’ve got dealt with virtually all of the instances that Angular’s async pipe handles besides the guarantees (but in addition that may be accomplished simply)! And understood why Angular async pipe will not be pure, and why that isn’t a problem in any respect!

I hope you favored this submit and realized one thing from it!

I tweet so much about Angular (newest information, movies, podcasts, updates, RFCs, pull requests and a lot extra). Should you’re about it, give me a comply with at @Enea_Jahollari. Give me a comply with on style-tricks.com should you favored this text and need to see extra like this!

Thanks for studying!



The Article was Inspired from tech community site.
Contact us if this is inspired from your article and we will give you credit for it for serving the community.

This Banner is For Sale !!
Get your ad here for a week in 20$ only and get upto 10k Tech related traffic daily !!!

Leave a Reply

Your email address will not be published. Required fields are marked *

Want to Contribute to us or want to have 15k+ Audience read your Article ? Or Just want to make a strong Backlink?