Latest Angular RFCs: Control Flow & Deferred Loading

Angular team presented two new RFCs for incrementally innovating Angular: One is about control flow; the other one about deferred loading.

During this year's ng-conf, the Angular team presented two new RFCs for incrementally innovating Angular. The first one is about new control flow primitives that are alternatives to control flow directives like *ngIf, *ngFor, and *ngSwitch. The second RFC about deferred loading helps to catch up with modern hydration concepts and has a lot of potential for improving startup times.

In this article, I give an overview of these ideas. In the end, I discuss what that means for us Angular developers.

Why Do We Need New Control Flow Primitives?

Currently, our templates use structural directives for control flow. An example is *ngIf:

<ng-container *ngIf="userName === 'Chuck Norris'; else someoneElse">
    Thanks for participating in this contest. Congrats, you are the winner!
</ng-container>
<ng-template #someoneElse>
    Thanks for participating in this contest. We have received your message and will evaluate it during the next couple of decades.
</ng-template>

Such classic control flow directives just augment our markup. This is quite unobtrusive but can lead to several issues:

  • You need to import these directives or the CommonModule into your Standalone Component.
  • If you don't want to introduce an element for your control flow directive, you need to go with ng-container, and this leads to more boilerplate.
  • The syntax for the else clause is a bit special: It's a template of its own structurally, not even connected to the *ngIf. The latter one just references it.
  • The *switch directive is even more special: A switch currently consists of three individual directives (ngSwitch, *ngSwitchCase, and *ngSwitchDefault), and this prevents type-safety in the template.

Honestly, this all is whining at a high level, and it's not critical to your application's success at all. However, while Signal Components will eventually allow going Zone-less, the classic control flow directives are not prepared for this. This means they'd need an overhaul anyway. Following the "Boy Scouting Rule" popularized by the clean code movement, the Angular team decided to improve control flow in general when adopting it for a possible Zone-less future.

This leads to the new control flow primitives the RFC presented at ng-conf 2023 is proposing.

The Proposed Control Flow Primitives

Using the proposed control flow primitives, the above-shown example would look as follows:

{#if userName === 'Chuck Norris'}
  Thanks for participating in this contest. Congrats, you are the winner!
{:else}
  Thanks for participating in this contest. We have received your message and will evaluate it during the next couple of decades.
{/if}

With {#xyz}, a new so-called block group is started; {:abc} starts a new block within the current block group, and {/xyz} closes the group. By definition, each block group has at least one block with the group's name. Hence, here we have an if group with an if block and an else block.

This mental model allows the composition of several blocks belonging together. In the previous listing, it is, for instance, obvious that the else block belongs to the if group. This was not fully the case in the first example using the classic control flow directives. This advantage becomes even more obvious when we want to go with an else if block too:

{#if userName === 'Chuck Norris'}
  Thanks for participating in this contest. Congrats, you are the winner!
{:else if isImportantPerson}
  Thanks for participating in this contest. We will reach out asap!
{:else}
  Thanks for participating in this contest. We have received your message and will evaluate it during the next couple of decades.
{/if}

To come even closer to syntax known from the JavaScript world, we can use optional parenthesis:

{#if (userName === 'Chuck Norris')}
  Thanks for participating in this contest. Congrats, you are the winner!
{:else if (isImportantPerson)}
  Thanks for participating in this contest. We will reach out asap!
{:else}
  Thanks for participating in this contest. We have received your message and will evaluate it during the next couple of decades.
{/if}

This proposed syntax addresses the drawbacks mentioned in the previous section and aligns with the following goals the Angular team had in mind:

  • The syntax used is similar to JavaScript and distinct from HTML. Hence, developers use a familiar style and see at first glance that this is about control flow. Also, the chosen syntax helps with the latter one.
  • Several blocks are grouped in a block group. This allows for a more natural representation of if-elseif-else or switch-case-default blocks. Also, it allows checking the types across the blocks.
  • The classic control flow directives can be automatically converted to this new syntax in almost all cases.

Further Examples

The RFC provides several examples. To give you an overview of the possibilities, the following listings repeat two of them. The first one is an example of a switch:

{#switch cond.kind}
  {:case x}
    X case
  {:case y}
    Y case
  {:case z}
    Z case
  {:default}
    No case matched
{/switch}

The second example, also taken from the RFC, shows a for loop:

{#for item of items; track item.id}
  {{ item }}
{:empty}
  There were no items in the list.
{/for}

Two things are interesting here: The first one is the optional empty block being displayed when the collection iterated over is empty. The second one is that the Angular team is thinking about making the track clause mandatory, as several code reviews showed that the lack of using trackBy with *ngFor leads to severe performance issues.

Deferred Loading

The second RFC presented is about deferred loading. The described mechanisms allow one to lazily load specific parts of a page. By just loading the important parts upfront, the loading times can be optimized.

This RFC builds upon the syntax proposed by the control flow RFC. The following example taken from the deferred loading RFC demonstrates the usage of the envisioned syntax:

{#defer on viewport}
  <calendar-cmp />
{/defer}

This defer block will be lazily loaded. The on clause defines an event triggering lazy loading. The setting viewport means it will be loaded once the block is scrolled into the visible part of the page. Other settings proposed by the RFC are idle, interaction, immediate, timer(x), and hover. The x in timer defines after which time period lazy loading should kick in.

Besides the on clause, there also is a when clause defining a condition. Once this condition is true, the deferred part is loaded:

{#defer when sheduleLoaded}
  <calendar-cmp />
{/defer}

It's also possible to combine when and on. Here is a more elaborated example taken from the RFC showing additional optional blocks:

{#defer on interaction; prefetch on idle}
  <calendar-cmp />
{:placeholder}
  <img src="placeholder.png" />
{:loading}
  <div class="loading">Loading the calendar...</div>
{:error}
  <p> Failed to load the calendar </p>
  <p><strong>Error:</strong> {{$error.message}}</p>
{/defer}

This example uses a prefetch clause allowing us to decouple loading from rendering. This makes it possible to load a deferred part on an earlier event (under an earlier condition) so that it's available when needed. The placeholder block is already displayed when the page loads, the loading block is shown while loading the deferred block, and the error block defines the markup shown when lazy loading doesn't work.

Discussion

To prepare for a Zone-less future, Angular's control flow needs an overhaul. Following the "Boy Scouting Rule", the Angular team uses this opportunity to compensate for several drawbacks the current control flow directives have. This results in a new set of control flow primitives mimicking the syntax known from JavaScript.

As with all new Angular features, also the new primitives are just an option. The known control flow directives like *ngIf and *ngFor don't go away and will be supported by both traditional components and the envisioned Signal components as long as the application uses Zone.js. Also, there will be an automatic migration covering almost all cases.

The features envisioned by the deferred loading RFC are built on top of the control flow RFC. They allow one to lazily load parts of a page. This improves startup times and helps the framework catch up with the several flavors of hydration that are currently popular in the JavaScript community.

More on Modern Angular?

Learn all about Standalone Components in our free eBook:

  • The mental model behind Standalone Components
  • Migration scenarios and compatibility with existing code
  • Standalone Components and the router and lazy loading
  • Standalone Components and Web Components
  • Standalone Components and DI and NGRX

Please find our eBook here:

free ebook

Feel free to download it here now!