Angular @let declarations: Smart Template Subscriptions

Ilir Beqiri - Sep 2 - - Dev Community

For some time now, Angular has been living in its momentum and the Angular team has proven that it cares about its community. In the Angular v17, and the following minor releases, the Angular team delivered many great features, with one that stood out most, even though in the developer preview, was the new built-in block template syntax that simplified working with templates.

In the recent version, two long-awaited issues in the Angular repo were closed. Angular major version, v18, shipped the Unified Control State Change Events amongst other features, and the minor version, v18.1, took advantage of the block template syntax by adding a new built-in feature to the template known as the Template Local Variables, denoted by @let block.

Check the official blog post to know more about how @let variables are defined, work, limitations, and how they update their values.

In simple terms, Template Local Variables, allow Angular developers to declare variables in their templates, just like we do in the component’s class, streamlining how we write the logic in the template thereby bringing alternatives to some old template patterns, and introducing new use cases that were covered in this article from @eneajaho.

The motivation for this article came from a Reddit thread questioning whether @let declarations were needed and why they should be used.

You can find the take of the main Angular contributor, Matthieu Riegler, about this topic here.

In this article, I want to show a use case of these local template variables I found useful on a project I work on, where I need no more do the “caching” with the RxJS shareReplay operator from the component’s class for using same piece of data in different template sections.

Let’s dive in 🚀.


RxJS “caching” with shareReplay operator

While developing web applications, the most common thing developers do is make HTTP requests. In Angular, HTTP communication is done through an observable-based API, the popular HttpClient. Since in most cases, the fetched data are bound in the template, developers follow the declarative approach with Async pipe as the best practice — automatically subscribes to the observable in the template, and unsubscribes when the component is destroyed 👇:

...
@Component({
  ...
  template: `
    ...
    <main>
      ...            👇
      @if (todo$ | async; as todo) {
        <p>Title: {{todo.title}}</p>
      }
    </main>
   ...
  `,
  standalone: true,
  ...
})
export class ShareReplayComponent {
  todo$ = inject(HttpClient)
    .get<Todo>('https://jsonplaceholder.typicode.com/todos/1');
}
Enter fullscreen mode Exit fullscreen mode

But there are cases when we need the data from the same stream somewhere else in the template, so we bind the observable stream in the template with the Async pipe again 👇:

...
@Component({
  template: `
    ...
    <main>
      ...           👇
      @if (todo$ | async; as todo) {
        <p>Title: {{todo.title}}</p>
      }
    </main>

    <aside>
      ...            👇
      @if (todo$ | async; as todo) {
        <p>Is Completed: {{todo.completed}}</p>
      }
    </aside>
   ...    
  `,
  standalone: true,
})
export class ShareReplayComponent {
  todo$ = inject(HttpClient)
    .get<Todo>('https://jsonplaceholder.typicode.com/todos/1');
}
Enter fullscreen mode Exit fullscreen mode

This results in having the same observable stream bound and subscribed in two different sections of the template thus having two, duplicate HTTP requests ongoing for fetching the same piece of data unnecessarily 👇:

Duplicate HTTP requests for the same observable stream bound twice in the template<br>

A common solution to this case (I’ve seen) is caching the data from the first fired HTTP request using RxJS through the shareReplay operator:

...
@Component({
  template: `
    ...
    <main>
      ...            👇
      @if (todo$ | async; as todo) {
        <p>Title: {{todo.title}}</p>
      }
    </main>

    <aside>
      ...            👇
      @if (todo$ | async; as todo) {
        <p>Is Completed: {{todo.completed}}</p>
      }
    </aside>
   ...
  `,
  standalone: true,

})
export class ShareReplayComponent {
  todo$ = inject(HttpClient)
    .get<Todo>('https://jsonplaceholder.typicode.com/todos/1')
    .pipe(shareReplay(1)); 👈
}
Enter fullscreen mode Exit fullscreen mode

This ensures that even though the same observable stream is bound and subscribed with the Async pipe in several places in the template, only one HTTP request will be triggered, and the response data gets cached 👇:

Caching HTTP request data with shareReplay RxJS operator

This pattern works fine but can we achieve this functionality more simply?

Let’s find out 💪.

@let declarations for streamlining

Although the RxJS solution works fine and fulfills our needs, the @let declarations introduced in Angular v18.1 offer a simpler, template-based alternative 👇:

...
@Component({
  template: `
    ...
    @let todo = todo$ | async; 👈
    <main>
      ...
      @if (todo) {
        <p>Title: {{todo.title}}</p>
      }
    </main>

    <aside>
      ...
      @if (todo) {
        <p>Is Completed: {{todo.completed}}</p>
      }
    </aside>
   ...
  `,
  standalone: true,

})
export class LetVariablesComponent {
  todo$ = inject(HttpClient)
    .get<Todo>('https://jsonplaceholder.typicode.com/todos/1');
}
Enter fullscreen mode Exit fullscreen mode

As can be noticed, it offers a kind of “template-based caching” — bind and subscribe to the HTTP request observable only once in the template 👇:

Avoid duplicate HTTP requests using @let local variables

As a result, no duplicate HTTP requests are outgoing, and no RxJS caching through the shareReplay operator is required. 🚀🚀

Note💡: This solution works when caching data for the template. The shareReplay operator is required if cached data is needed in the component’s class.


Special thanks to @kreuzerk and @eneajaho for the review.

Thanks for reading!

I hope you enjoyed it 🙌. If you liked the article please share it with your friends and colleagues.

For any questions or suggestions, feel free to comment below 👇.

If this article is interesting and useful to you, and you don’t want to miss future articles, follow me at @lilbeqiri, dev.to, or Medium. 📖

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .