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
ngFor
is 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
Basic Usageโ
rxFor
accepts ObservableInput
, Signal
as well as static values
- Usage with signals
- Usage with observables
- Usage with static values
<div class="movie-list">
<movie *rxFor="let movie of movies;" [movie]="movie" />
</div>
import { RxFor } from '@rx-angular/template/for';
import { Component } from '@angular/core';
@Component({
templateUrl: './list.component.html',
standalone: true,
imports: [RxFor],
})
export class ListComponent {
movies: Signal<Movie[]> = this.movieService.fetchMovies();
}
<div class="movie-list">
<movie *rxFor="let movie of movies$;" [movie]="movie" />
</div>
import { RxFor } from '@rx-angular/template/for';
import { Component } from '@angular/core';
@Component({
templateUrl: './list.component.html',
standalone: true,
imports: [RxFor],
})
export class ListComponent {
movies$: Observable<Movie[]> = this.movieService.fetchMovies();
}
<div class="menu">
<menu-item *rxFor="let item of menuItems;" [item]="item" />
</div>
<!-- unwrapping signals also counts as static value -->
<div class="movie-list">
<movie *rxFor="let movie of movies();" [movie]="movie" />
</div>
import { RxFor } from '@rx-angular/template/for';
import { Component } from '@angular/core';
@Component({
templateUrl: './list.component.html',
standalone: true,
imports: [RxFor],
})
export class ListComponent {
menuItems: MenuItem[] = [
{
id: 1,
title: 'edit',
},
{
id: 2,
title: 'delete',
},
];
moviesSignal: Signal<Movie[]> = this.movieService.fetchMovies();
}
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.
Save code with the trackBy
shortcutโ
- trackBy shortcut
- trackByFunction
You can pass any valid property from the given input type as a shortcut instead of providing a TrackByFunction
<div class="movie-list">
<movie *rxFor="let movie of movies$; trackBy: 'id' " [movie]="movie" />
</div>
import { RxFor } from '@rx-angular/template/for';
import { Component } from '@angular/core';
@Component({
templateUrl: './list.component.html',
standalone: true,
imports: [RxFor],
})
export class ListComponent {
movies$: Observable<Movie[]> = this.movieService.fetchMovies();
}
<div class="movie-list">
<movie *rxFor="let movie of movies$; trackBy: trackMovie" [movie]="movie" />
</div>
import { RxFor } from '@rx-angular/template/for';
import { Component } from '@angular/core';
@Component({
templateUrl: './list.component.html',
standalone: true,
imports: [RxFor],
})
export class ListComponent {
movies$: Observable<Movie[]> = this.movieService.fetchMovies();
trackMovie(i: number, movie: Movie) {
return movie.id;
}
}
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>
Conceptsโ
- Local variables
- Handling view and content queries
- NgZone optimizations
- Render strategies especially the section usage-in-the-template
Featuresโ
DX Features
- reduces boilerplate (multiple
async
pipe's) - a unified/structured way of handling
null
andundefined
- 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
Input | Type | description |
---|---|---|
trackBy | keyof T or (index: number, item: T) => any | Identifier function for items. rxFor provides a shorthand where you can name the property directly. |
patchZone | boolean | default: true if set to false , the RxFor will operate out of NgZone . See NgZone optimizations |
parent (deprecated) | boolean | default: 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 |
strategy | Observable<RxStrategyNames \ string> \ RxStrategyNames \ string> | default: normal configure the RxStrategyRenderStrategy used to detect changes. |
renderCallback | Subject<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. |
Context Variablesโ
The following context variables are available for each template:
Static Context Variables (mirrored from ngFor
)
Variable Name | Type | description |
---|---|---|
$implicit | T | the default variable accessed by let val |
index | number | current index of the item |
count | number | count of all items in the list |
first | boolean | true if the item is the first in the list |
last | boolean | true if the item is the last in the list |
even | boolean | true if the item has on even index (index % 2 === 0) |
odd | boolean | the opposite of even |
Reactive Context Variables
Variable Name | Type | description |
---|---|---|
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. |
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
)โ
The parent
flag being true is not needed anymore with the new signal based view queries.
The flag itself is deprecated now and will be removed in future versions.
However, for the time being: if you are already using the signal queries, you definitely want to set the parent
flag to be false. We highly recommend doing so, as it reduces the amount of
change detection cycles significantly, thus improving the runtime performance of your apps.
You can do so by providing a custom RxRenderStrategiesConfig
, see the following example:
// import
import { RxRenderStrategiesConfig, RX_RENDER_STRATEGIES_CONFIG } from '@rx-angular/cdk/render-strategies';
// create configuration with parent flag to be false
const rxaConfig: RxRenderStrategiesConfig<string> = {
parent: false,
};
// provide it, in best case on root level
{
providers: [
{
provide: RX_RENDER_STRATEGIES_CONFIG,
useValue: rxaConfig,
},
];
}
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.
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.
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
.
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's
EmbeddedViewsinside
NgZone`.
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