Skip to main content

RxLet

RxLet (*rxLet) binds an Observable, Signal, or plain value to the template and exposes a built-in suspense / error / complete reactive context alongside per-directive render scheduling. It does more than the native @let, which only names a value: rxLet tracks the state of an asynchronous source (still loading, failed, completed) and renders it through a cancelable, frame-budgeted scheduler.

Why this matters: see The reactive context for the suspense/error/complete model, and Concurrent scheduling & the frame budget for how per-directive scheduling works.

Import

import { RxLet } from '@rx-angular/template/let';

Usage

Add RxLet to a standalone component's imports and bind a reactive source with the native structural-directive let syntax.

src/counter.component.ts
import { Component, signal } from '@angular/core';
import { RxLet } from '@rx-angular/template/let';

@Component({
selector: 'app-counter',
imports: [RxLet],
template: `
<ng-container *rxLet="number; let n">
<app-number [number]="n" />
<app-number-special [number]="n" />
</ng-container>
`,
})
export class CounterComponent {
number = signal(0);
}

The same binding works with an Observable:

import { BehaviorSubject } from 'rxjs';

export class CounterComponent {
number$ = new BehaviorSubject(0);
// template: <ng-container *rxLet="number$; let n"> … </ng-container>
}

*rxLet always binds the value, including falsy values (0, '', false, null, undefined). This differs from @if (number$ | async; as n), which hides the content on a falsy value, a common source of edge-case bugs. Use @let or @if (… | async) for a plain value; reach for *rxLet when you also want the reactive context or render scheduling below.

By default *rxLet is optimized for performance: it renders through the normal render strategy (non-blocking, frame-budgeted) and creates templates lazily. See tuning rendering with strategies to change it.

Inputs

Value

InputTypeDescription
rxLetObservableInput<U> \| Subscribable<U> \| Signal<U> \| U \| null \| undefinedThe Observable, Signal, or value to bind to the template context.

Contextual state

InputTypeDescription
errorTemplateRef<RxLetViewContext>Defines the template for the error state.
completeTemplateRef<RxLetViewContext>Defines the template for the complete state.
suspenseTemplateRef<RxLetViewContext>Defines the template for the suspense state.
nextTriggerObservable<unknown>Trigger to show the next template.
errorTriggerObservable<unknown>Trigger to show the error template.
completeTriggerObservable<unknown>Trigger to show the complete template.
suspenseTriggerObservable<unknown>Trigger to show the suspense template.
contextTriggerObservable<RxNotificationKind>Trigger to show any template, based on the given RxNotificationKind.

Rendering

InputTypeDescription
patchZonebooleandefault: true if set to false, RxLet operates outside NgZone (zoneful apps only). See tuning rendering with strategies.
parent (deprecated)booleandefault: false if true, RxLet informs its host component about template changes so legacy @ViewChild/@ContentChild decorator queries update. Deprecated: not needed with signal-based view queries.
strategyObservable<RxStrategyNames \| string> \| RxStrategyNames \| stringdefault: normal the render strategy used to detect changes. See tuning rendering with strategies.
renderCallbackNextObserver<U>Notifies when RxLet created, updated, or removed its template; useful when you need to know rendering is done.

Context

RxLet exposes the reactive context state in the template two ways: context variables and context templates.

The full reactive context (suspense, error, complete) is only derived from Observable sources. For a Signal source, only suspense and error are derived.

Context variables

Each template exposes these context variables:

  • $implicit: T, the default value, accessed by let n
  • error: boolean \| Error
  • complete: boolean
  • suspense: boolean
<ng-container *rxLet="number$; let n; let s = suspense; let e = error; let c = complete">
{{ s && 'No value arrived so far' }}
<app-number [number]="n" />
There is an error: {{ e ? e.message : 'No Error' }} Observable is completed: {{ c ? 'Yes' : 'No' }}
</ng-container>

Context templates

Template anchors render the contextual state per state:

<ng-container
*rxLet="
number$; let n;
error: error;
complete: complete;
suspense: suspense;
"
>
<app-number [number]="n" />
</ng-container>

<ng-template #suspense>SUSPENSE</ng-template>
<ng-template #error>ERROR</ng-template>
<ng-template #complete>COMPLETE</ng-template>

Triggers

Trigger inputs let you switch the displayed template from asynchronous code (a Promise or Observable), ideal for a searchable list with a loading spinner. When a trigger fires it applies the new context state, updates the local variables, and switches to the registered template if one exists.

nextTrigger

Switch back from any template to the value display.

@Component({
selector: 'app-root',
imports: [RxLet],
template: `
<button (click)="nextTrigger$.next()">show value</button>
<ng-container *rxLet="num$; let n; complete: complete; nextTrigger: nextTrigger$">
{{ n }}
</ng-container>
<ng-template #complete>✔</ng-template>
`,
})
export class AppComponent {
nextTrigger$ = new Subject();
num$ = timer(2000);
}

errorTrigger

Show the error template.

@Component({
selector: 'app-root',
imports: [RxLet],
template: `
<ng-container *rxLet="num$; let n; error: error; errorTrigger: errorTrigger$">
{{ n }}
</ng-container>
<ng-template #error>❌</ng-template>
`,
})
export class AppComponent {
private state = inject(GlobalState);
num$ = this.state.num$;
errorTrigger$ = this.state.error$;
}

completeTrigger

Show the complete template.

@Component({
selector: 'app-root',
imports: [RxLet],
template: `
<ng-container *rxLet="num$; let n; complete: complete; completeTrigger: completeTrigger$">
{{ n }}
</ng-container>
<ng-template #complete>✔</ng-template>
`,
})
export class AppComponent {
private state = inject(GlobalState);
num$ = this.state.num$;
completeTrigger$ = this.state.success$;
}

suspenseTrigger

Show the suspense template.

@Component({
selector: 'app-root',
imports: [RxLet],
template: `
<input (input)="search($event.target.value)" />
<ng-container *rxLet="num$; let n; suspense: suspense; suspenseTrigger: suspenseTrigger$">
{{ n }}
</ng-container>
<ng-template #suspense>loading...</ng-template>
`,
})
export class AppComponent {
private state = inject(GlobalState);
num$ = this.state.num$;
suspenseTrigger$ = new Subject();

search(str: string) {
this.state.search(str);
this.suspenseTrigger$.next();
}
}

contextTrigger

Set any context in one input. It combines suspenseTrigger, completeTrigger, and errorTrigger, driven by an RxNotificationKind.

@Component({
selector: 'app-root',
imports: [RxLet],
template: `
<input (input)="search($event.target.value)" />
<ng-container *rxLet="num$; let n; suspense: suspense; contextTrigger: contextTrigger$">
{{ n }}
</ng-container>
<ng-template #suspense>loading...</ng-template>
`,
})
export class AppComponent {
private state = inject(GlobalState);
num$ = this.state.num$;
contextTrigger$ = new Subject();

search(str: string) {
this.state.search(str);
this.contextTrigger$.next(RxNotificationKind.Suspense);
}
}

Features

  • Context variables (error, complete, suspense)
  • Context templates (error, complete, suspense)
  • Context triggers
  • Reduces boilerplate compared to multiple async pipes
  • A unified, structured way to handle null and undefined
  • Works with static values: *rxLet="42; let n"
  • Value binding is always present, with no falsy-value hiding
  • Lazy template creation (via render strategies)
  • Change detection on the EmbeddedView level
  • Distinct consecutive equal values are skipped (over-render guard)
  • Concurrent, frame-budgeted rendering (see concurrent scheduling)

See also