Skip to main content

RxFor

Motivationโ€‹

The most common way to render lists in angular is by using the *ngFor structural directive. *ngFor is able to take an arbitrary list of data and repeat a defined template per item of the list. However, it can only do it synchronously.

Compared to the NgFor, RxFor treats each child template as single renderable unit. The change detection of the child templates get prioritized, scheduled and executed by leveraging RenderStrategies under the hood. This technique enables non-blocking rendering of lists and can be referred to as concurrent mode.

Read more about this in the strategies section.

Furthermore, RxFor provides hooks to react to rendered items in form of a renderCallback: Subject.

Together with the RxRenderStrategies, this makes the rendering behavior extremely versatile and transparent for the developer. Each instance of RxFor can be configured to render with different settings.

Downsides

  • Bootstrapping of ngForis slow
  • Change detection and render work processed in a UI blocking way
  • Laziness of DOM is not given (slow template creation)
  • Nested structures are very slow, especially with updates
  • Destruction is more computation heavy than adding bootstrapping

Conceptsโ€‹

Featuresโ€‹

DX Features

  • reduces boilerplate (multiple async pipe's)
  • a unified/structured way of handling null and undefined
  • works also with static variables *rxFor="let i of []"
  • Immutable as well as mutable data structures (trackBy)
  • Provide a comprehensive set of context variables

Performance Features

  • lazy template creation (done by render strategies)
  • non-blocking rendering of lists
  • configurable frame budget (defaults to 60 FPS)
  • triggers change-detection on EmbeddedView level
  • distinct same values in a row (over-rendering)
  • ListManager: special logic for differ mechanism to avoid over-rendering; abstracts away low level logic
  • cancel any scheduled work if a remove was triggered for a trackById
  • cancel any update if a new update was triggered for the same trackById
  • nested lists will items fine grained and re-render only what is needed

Inputsโ€‹

Rendering

InputTypedescription
trackBykeyof T or (index: number, item: T) => anyIdentifier function for items. rxFor provides a shorthand where you can name the property directly.
patchZonebooleandefault: true if set to false, the RxFor will operate out of NgZone. See NgZone optimizations
parentbooleandefault: true if set to false, the RxFor won't inform its host component about changes being made to the template. More performant, @ViewChild and @ContentChild queries won't work. Handling view and content queries
strategyObservable<RxStrategyNames \ string> \ RxStrategyNames \ string>default: normal configure the RxStrategyRenderStrategy used to detect changes.
renderCallbackSubject<U>giving the developer the exact timing when the RxFor created, updated, removed its template. Useful for situations where you need to know when rendering is done.

Outputsโ€‹

  • n/a

Context Variablesโ€‹

The following context variables are available for each template:

Static Context Variables (mirrored from ngFor)

Variable NameTypedescription
$implicitTthe default variable accessed by let val
indexnumbercurrent index of the item
countnumbercount of all items in the list
firstbooleantrue if the item is the first in the list
lastbooleantrue if the item is the last in the list
evenbooleantrue if the item has on even index (index % 2 === 0)
oddbooleanthe opposite of even

Reactive Context Variables

Variable NameTypedescription
item$Observable<T>the same value as $implicit, but as Observable
index$Observable<number>index as Observable
count$Observable<number>count as Observable
first$Observable<boolean>first as Observable
last$Observable<boolean>last as Observable
even$Observable<boolean>even as Observable
odd$Observable<boolean>odd as Observable
select(keys: (keyof T)[], distinctByMap) => Observable<Partial<T>>returns a selection function which accepts an array of properties to pluck out of every list item. The function returns the selected properties of the current list item as distinct Observable key-value-pair.

Setupโ€‹

The RxFor can be imported as following:

import { RxFor } from "@rx-angular/template/for";

@Component({
standalone: true,
imports: [RxFor],
template: `...`
})
export class AnyComponent {}

Basic Usageโ€‹

โš  Notice: By default *rxFor is optimized for performance out of the box.

This includes:

  • The default render strategy is normal. This ensures non-blocking rendering but can cause other side-effects. See strategy configuration if you want to change it.
  • Creates templates lazy and manages multiple template instances

As a list can take larger to render items can appear in batches if concurrent strategies are used. This brings several benefits. e.g. stop rendering in between and navigate away.

Simple example using *rxFor with Observable valuesโ€‹

@Component({
template: `
<ul>
<li *rxFor="let item of items$; trackBy: trackItem">{{ item }}</li>
</ul>
`
})
export class AnyComponent {
items$: Observable<Items[]> = getItems();

trackItem(, item) {
return item.id
}
}

Simple example using *rxFor with simple static valuesโ€‹

๐Ÿ”ฅ Perf Tip: As rxFor accepts also static values it can serve as a drop in replacement with an easy find and replace refactoring.

@Component({
template: `
<ul>
<li *rxFor="let item of items; trackBy: trackItem">{{ item }}</li>
</ul>
`
})
export class AnyComponent {
items: Items[] = getItems();

trackItem(, item) {
return item.id
}
}

Save code with the trackBy shortcutโ€‹

๐Ÿ’ก DX Tip: As rxFor accepts also static values it can serve as a drop in replacement with an easy find and replace refactoring.

@Component({
template: `
<ul>
<li *rxFor="let item of items; trackBy: 'id'">{{ item }}</li>
</ul>
`,
})
export class AnyComponent {}

Using the static context variablesโ€‹

<ul>
<li
*rxFor="
let item of observableItems$; trackBy: 'id';
let count = count;
let index = index;
let first = first;
let last = last;
let even = even;
let odd = odd;
"
>
<div>{{ count }}</div>
<div>{{ index }}</div>
<div>{{ item }}</div>
<div>{{ first }}</div>
<div>{{ last }}</div>
<div>{{ even }}</div>
<div>{{ odd }}</div>
</li>
</ul>

Using the reactive context variablesโ€‹

<ul>
<li
*rxFor="
let item of observableItems$; trackBy: 'id';
let count$ = count$;
let index$ = index$;
let first$ = first$;
let last$ = last$;
let even$ = even$;
let odd$ = odd$;
"
>
<div *rxLet="count$; let c">{{ c }}</div>
...
</li>
</ul>

Advanced Usageโ€‹

Use render strategies (strategy)โ€‹

You can change the used RenderStrategy by using the strategy input of the *rxFor. It accepts an Observable<RxStrategyNames> or RxStrategyNames.

The default value for strategy is normal.

<ng-container *rxFor="let item of items; strategy: strategy">
{{ item }}
</ng-container>

<ng-container *rxFor="let item of items; strategy: strategy$">
{{ item }}
</ng-container>
@Component({
/**/
})
export class AppComponent {
strategy = 'low';
strategy$ = of('immediate');
}

Learn more about the general concept of RenderStrategies especially the section usage-in-the-template if you need more clarity.

Local strategies and view/content queries (parent)โ€‹

When local rendering strategies are used, we need to treat view and content queries in a special way. To make *rxFor in such situations, a certain mechanism is implemented to execute change detection on the parent (parent).

This is required if your components state is dependent on its view or content children:

  • @ViewChild
  • @ViewChildren
  • @ContentChild
  • @ContentChildren

Imagine the following situation:

@Component({
selector: 'app-list-component',
template: ` <ng-content select="app-list-item"></ng-content>`,
})
export class AppListComponent {
@ContentChildren(AppListItemComponent);
appListItems: QueryList<AppListItemComponent>;
}

The usage of AppListComponent looks like this:

<app-list-component>
<app-list-item
*rxFor="
let item of observableItems$;
parent: true;
"
>
<div>{{ item }}</div>
</app-list-item>
</app-list-component>

Read more about this at handling view and content queries

RxFor with concurrent strategiesโ€‹

The *rxFor directive is configured to use the normal concurrent strategy by default.

Rendering large sets of data is and has always been a performance bottleneck, especially for business applications.

common problem

The most common way to render lists in angular is by using the *ngFor structural directive. *ngFor is able to take an arbitrary list of data and repeat a defined template per item of the list. However, it can only do it synchronously. In other words, the larger the set of data or the heavier the template to repeat, the more blocking the user experience of your application will be.

blocking ng-for

The *rxFor structural directive provides a convenient and performant way for rendering templates out of a list of items.

Input values can be provided either as Observable, Promise or static values.

Compared to the NgFor, RxFor treats each child template as single renderable unit. The change detection of the child templates get prioritized, scheduled and executed by leveraging RenderStrategies under the hood. This technique enables non-blocking rendering of lists and can be referred to as concurrent mode.

rxFor improvement rxFor usage

As rendering of each template will be processed as individual task, rendering can be cancelled.

Use the renderCallbackโ€‹

The renderCallback can be seen as hook into the change detection system. It's essentially a Subject which emits whenever *rxFor finished rendering a set changes to the view. This enables developers to perform actions when a list has finished rendering. The renderCallback is useful in situations where you rely on specific DOM properties like the height a table after all items got rendered, or to adjust scroll-positions. It is also possible to use the renderCallback in order to determine if a view should be visible or not. This way developers can hide a list as long as it has not finished rendering.

The result of the renderCallback will contain the currently rendered set of items in the iterable.

@Component({
selector: 'app-root',
template: `
<app-list-component>
<app-list-item
*rxFor="
let item of items$;
trackBy: trackItem;
renderCallback: itemsRendered
"
>
<div>{{ item.name }}</div>
</app-list-item>
</app-list-component>
`,
})
export class AppComponent {
items$: Observable<Item[]> = itemService.getItems();
trackItem = (idx, item) => item.id;
// this emits whenever rxFor finished rendering changes
itemsRendered = new Subject<Item[]>();

constructor(elementRef: ElementRef<HTMLElement>) {
itemsRendered.subscribe(() => {
// items are rendered, we can now scroll
elementRef.scrollTo({ bottom: 0 });
});
}
}

Nested rxFor and the select variableโ€‹

This example showcases the select view-context function used for deeply nested lists.

<ul>
<li *rxFor="let hero of heroes$; trackBy: trackItem; let select = select;">
<div>
<strong>{{ hero.name }}</strong></br>
Defeated enemies:
</div>
<span *rxFor="let enemy of select(['defeatedEnemies']); trackBy: trackEnemy;">
{{ enemy.name }}
</span>
</li>
</ul>

This will significantly improve the performance.

Working with event listeners (patchZone)โ€‹

A flag to control whether rxFor templates are created within NgZone or not. The default value is true, rxForwill create it'sEmbeddedViewsinsideNgZone`.

Event listeners normally trigger zone. Especially high frequently events cause performance issues.

For more details read about NgZone optimizations

@Component({
selector: 'app-root',
template: `
<div
*rxFor="let bgColor; in: bgColor$; patchZone: false"
(mousemove)="calcBgColor($event)"
[style.background]="bgColor"
></div>
`,
})
export class AppComponent {
// As the part of the template where this function is used as event listener callback
// has `patchZone` false the all event listeners run outside zone.
calcBgColor(moveEvent: MouseEvent) {
// do something with the background in combination with the mouse position
}
}

Testingโ€‹

Handling the scheduling issueโ€‹

By default *rxFor uses the normal concurrent strategy which runs change detection asynchronously. This behavior can lead to unexpected results in test environments. We recommend to test your templates using the native strategy to avoid this problem.

This can be configured as a StaticProvider.

Setting the default strategy

export const RX_ANGULAR_TEST_PROVIDER: StaticProvider = {
provide: RX_RENDER_STRATEGIES_CONFIG,
useValue: {
primaryStrategy: 'native',
},
};

Overriding a strategy

There will be cases where you have assigned a custom strategy and the primaryStrategy setting won't do anything for you.

In order to still use the native strategy in your test environment, you can simply override the custom strategy with the native one.

export const RX_ANGULAR_TEST_PROVIDER: StaticProvider = {
provide: RX_RENDER_STRATEGIES_CONFIG,
useValue: {
primaryStrategy: 'native',
customStrategies: {
userBlocking: {
...RX_NATIVE_STRATEGIES.native,
name: 'userBlocking',
},
},
},
};

If you have done your desired configuration, declare it in the providers entry of the TestModule.

TestBed.configureTestingModule({
...
providers: [RX_ANGULAR_TEST_PROVIDER],
}).compileComponents();

This way, *rxFor will use the same rendering strategy used by the Angular's built-in async pipe.

Resourcesโ€‹

Demos: A showcase for blocking UI as a stackblitz demo Feature demos in our demos app

Example applications: A real live application using rxFor is available on GitHub.

Design docs, Researches, Case Studies This issue documents how we approached rxFor