One more async pipe syntax trick

We’ve covered a few different async pipe syntax tricks earlier in this newsletter, as well as why it’s important to use the async pipe to automate your subscriptions/unsubcriptions from Observables.

Today, let’s see another interesting syntax that can be very helpful when a template has multiple subscriptions, such as:

Even if using ng-container, as we saw earlier this week, is helpful, it would be even better to have just one place where both subscriptions happen.

We can simplify the above by creating a new object that has one property for each Observable value as follows:

That way, all our async subscriptions are in one place, and the rest of our HTML template is much more readable.

What is ng-container?

Yesterday, we covered ng-template and touched on different scenarios when it makes sense to use ng-template. Let’s now introduce a close relative of ng-template called ng-container.

There are a lot of similarities between those two elements, which can both be used to use structural directives such as ngIf or ngFor, with the bonus that ng-container can be used with the shorthand syntax of those directives:

One thing you probably noticed as an Angular developer is that we can’t use two structural directives on the same element:

In this scenario, using ng-container is the perfect way to use both directives without adding any HTML to the DOM, as ng-container, just like ng-template, doesn’t create new DOM elements:

Another use for ng-container is to become the host of a ng-template, similar to the way that a router-outlet is the host of routed components:

Such templates can be passed from a parent component using @Input and let the child component decide when to display which template, which is very powerful:

You can find a complete code example of such a feature in this Stackblitz and a more in-depth tutorial here if you want to try it.

Using services to cache data

Yesterday, we saw that services are singletons that are created once for all and stay in memory as long as the application is open.

This makes our services the best place to store data that can be shared between multiple different components. Such services can use BehaviorSubjects or Signals to cache that data and replay the last value automatically to any new subscriber (component, directive, pipe, service, etc.).

Here is an example using a signal to store a value and expose it in a read-only fashion:

This article compares BehaviorSubjects and Signals to cache a shareable value.

Lifecycle of Angular applications

The lifecycle of an Angular application is something that many aspiring Angular developers struggle with. People often ask me questions such as:

  • How long does this component/service/directive stay in memory?
  • How do I save the data before I navigate to the next page/view/component?
  • What happens if I open that same app in another browser tab?

Here is how to think about it:

  • When we open an app in a browser tab, we’re booting an Angular application in a self-contained memory space, similar to a virtual machine.
  • Closing that tab is equivalent to killing the application, freeing any memory associated with it, just like when you close a desktop app in your machine’s operating system.

In Google Chrome, there’s even a task manager where you can see the memory footprint and CPU usage of your tabs and browser extensions – they’re just like independent desktop apps:

From an Angular perspective, a component gets loaded in memory whenever displayed on the screen. That’s when its class is instantiated.

Suppose the component gets removed from the screen (by navigating to a different component or removing it with a ngIf directive, for instance). In that case, the component is destroyed, and all of its memory state is gone. The same goes for directives and pipes: They get created when used by a component template and destroyed when that component gets destroyed.

Services are different, though. A service is created by Angular when a component needs it for the first time. Then, that instance remains unique and shared between all components that inject such service. A service doesn’t get destroyed: It remains in memory as long as your app is open and you don’t close your browser or tab.

This answers our three initial questions:

  • How long does this component/service/directive stay in memory?
    Components stay as long as they’re in the DOM. Services stay as long as the app is open.
  • How do I save the data before I navigate to the next page/view/component?
    Not if you save it in a service
  • What happens if I open that same app in another browser tab?
    You create another separate instance of everything: Components, services, etc. Both instances are independent and do not share any data or memory space.

Using setters for better @Input

When using @Input in our components, we can implement ngOnChanges to be notified whenever an input value has changed:

The above code works and would run whenever id or name are updated by the parent component. The problem is that this syntax can quickly become heavy when there are several @Inputs in your component.

Instead, we can do this:

Instead of using @Input() on a class property, we can use it on an ES6 setter function. The benefit of the setter syntax is that it makes it obvious what kind of code we’re running when the @Input value is set. No more ngOnChanges needed.

Build size budgets

As mentioned earlier in this newsletter, the size of your Javascript matters a lot, as our code has to be downloaded first, then parsed and interpreted by a browser, which will get slower and slower as the size of your app increases. This is why we want optimized production builds. And this is also why it’s always a good idea to keep track of the size of your production code after each build.

Fortunately, the Angular team has our back and integrated build budgets in the Angular CLI. You can use those budget settings to decide when to get a warning or even fail a build it your code becomes too big. This configuration happens in angular.json:

The above (default) budgets would warn you if your Javascript exceeds 500Kb in size and fail once the build exceeds 1Mb. Those are already part of your projects, so you don’t have to do anything to use them.

If you do continuous integration, your build will fail right after the commit that degraded your bundle size, making it easy to troubleshoot and fix the issue.

Most of the time, dependencies are the culprits. I remember coaching a client who needed some Excel-like features in the browser, and their build exploded to over 25MB because of a massive monolithic Javascript Excel library. Thanks to the build error, we knew that this library wouldn’t work, so we chose a lighter one instead.

In the past, I’ve also inherited projects where I would track our build size version after version. My client was amazed to see that despite adding features, the build was getting smaller and smaller after each release, thanks to Angular always being better at tree-shaking and having the incentive to clean up old code and make it smaller. When you start tracking a metric, you want to improve it!

Angular style guide

People often ask me the best way to organize their files/folders. While it’s mostly a matter of personal preference or a team decision, the recommendations of the Angular style guide are always good to follow.

For instance, when it comes down to folders, here’s what the official style guide says (the highlight is mine, as I find this to be true for myself):

What’s great about the Angular style guide is that it always provides why such approaches are recommended. As you can see, the above justifications make perfect sense.

Also, while some recommendations are purely naming conventions, others touch on architecture decisions, such as talking to the server through a service or using directives to enhance an element (somewhat similar to my earlier call to think more about directives instead of components).

ngFor trackBy for improved performance

We all use the ngFor directive on a daily basis. The directive is used to repeat a template as many times as needed based on an array of data we want to render. We covered how the directive exposes useful local variables in the past.

To help Angular perform DOM updates when items in the array are reordered, new items are added, or existing items are removed, we can add a trackBy attribute that specifies what unicity criteria Angular will use to track these additions/removals/reorderings:

Of course, this only helps if the array in question is subject to changes in the future. By default, Angular uses Object.is() to compare values in the array, which is somewhat similar to a === comparison.

In our case, we created a function that specifies how to track by _id:

That way, Angular will know how to compare objects efficiently and won’t perform DOM updates that aren’t needed, which can be a huge performance boost if you’re displaying a massive data table with thousands of records. You can see this example in action on Stackblitz here.

Prefer template-driven forms

International readers might not know that today is a public holiday in the United States as the country celebrates its birth and Independence. As a result, I’ll make today’s post very short but still useful.

Over the past couple of newsletters, I explained that template-driven forms are perfectly fine in most cases. A few years ago at ng-conf 2021, Ward Bell said he always uses template-driven forms and explained how he does that in less than 19 minutes.

If you’d rather read a short article, you can head to my “reactive template-driven forms” post where I showcase how template-driven forms are actually… a layer on top of reactive forms, and that you could still access reactive features on a template-driven form if you wanted to. I know, it’s mind-blowing!

Happy 4th of July if you’re in the US!

Reactive forms Observables

Last week, I gave you a simple decision framework to decide when to use reactive or template-driven forms.

As a matter of fact, the only reactive (meaning RxJs-friendly) piece of reactive forms is that FormGroup, FormArray, and FormControl all extend the AbstractControl class and, as a result, expose two different Observables:

Those two can be very powerful when used alongside some RxJs wizardry (see this tutorial on dynamic filtering with RxJs and Angular forms, for example). In their most simple form, we can use those to listen to changes in our form and react accordingly:

In the above example, we enable the city form control if a 5-digit zip code has been entered. Since we are subscribed to valueChanges, any future change of the zipcode value could enable/disable the city input.

The other Observable, statusChanges, focuses on validity changes only and returns one of the four following values:

Here is an example use case for statusChanges, which is more accurate than the previous example as this one would take into account all validation code relevant to the zip code: