Förderjahr 2023 / Projekt Call #18 / ProjektID: 6798 / Projekt: RxAngular
The RxAngular team & community were quite productive and shipped great new features as well as quality of life improvements with the latest releases.
We are specifically mentioning the community this time, as we are very happy to anounce that some amazing community contributions made it to this release.
In this blog post, we'll dive into some specifics of RxAngular itself and it's recently added new features, including the native support for signals across its core directives: rxIf
, rxLet
, rxFor
, and rxVirtualFor
.
TL;DR
If you don't want to go over all the changes in detail, here is a tiny teaser for you with just the changes being introduced with the latest release.
@rx-angular/template v17.2.0
-
native signal support
-
rxLet supports Subscribable (*community contribution)
-
parent flag deprecation
@rx-angular/state v17.2.0
-
provideRxStateConfig
-
expose readonly state (*community contribution)
@rx-angular/state v17.1.0
-
native signal support
-
new functional creation method
The RxAngular Template Package Benefits
Before going into details about updates about the @rx-angular/template package
, let's first understand what is the difference between RxAngular directives and Angular directives when it comes to *ngIf, *ngFor vs *rxIf, *rxFor and *rxLet.
Fine-grained reactivity
RxAngular directives are optimized for fine-grained reactivity down to the EmbeddedViewRef. This means that only the necessary parts of your template are updated when data changes, leading to better performance.
Context variables and templates
RxAngular directives allow you to work with context variables and templates in a more intuitive way. This makes it easier to manage complex data structures and logic in your templates.
📣 Conference Talk: Contextual Template States
Lazy template creation & Concurrent Mode
RxAngular directives support lazy template creation, which can help improve performance by deferring the creation of template elements until they are actually needed.
Users want 60fps and smooth interactions with applications, and RxAngular directives are optimized for this. They ensure that your application remains responsive and performant even when dealing with large amounts of changes by making use of concurrent mode.
📣 Conference Talk: Cut My Task Into Pieces
RxAngular template directives now natively supports signals
RxAngular recently introduced seamless integration with Angular signals.
Understanding Angular Signals
Let's briefly recap what Angular signals are. Signals provide a reactive primitive for managing state within Angular components. They offer a fine-grained and performant way to track changes to values. Thus, angular has an easier time to run change detection only on needed components instead of a sub-tree.
RxAngulars Signal-Powered Directives
RxAngulars core directives are designed to streamline reactive programming in Angular templates. Now they have been supercharged with signal support. Let's explore how each directive leverages signals to enhance your development workflow:
rxLet
The rxLet
directive is your go-to solution for working with observables and signals in your templates. It allows you to subscribe to an observable or signal within the template and conveniently expose its emitted values to the template's context.
userData = signal({ name: 'Alice', email: 'alice@example.com' });
<div *rxLet="userData; let user">
<p>Welcome, {{ user.name }}!</p>
<p>Your email is: {{ user.email }}</p>
</div>
rxIf
The rxIf
directive is a powerful tool for conditionally rendering parts of your template based on the truthiness of a value. With signal support, you can now directly bind a signal to rxIf. When the signal's value changes, rxIf
will automatically re-evaluate the condition and update the DOM accordingly.
showContent = signal(true);
<div *rxIf="showContent">This content is displayed conditionally.</div>
rxFor
Efficiently rendering lists is a common requirement in web applications. The rxFor
directive simplifies this process by iterating over an array or an observable of arrays. Now, with signal support, you can directly bind a signal representing an array to rxFor
. As the array within the signal changes, rxFor
will intelligently update the list in your template.
items = signal(['apple', 'banana', 'orange']);
<ul>
<li *rxFor="let item of items">{{ item }}</li>
</ul>
rxVirtualFor
When dealing with large lists, virtual scrolling becomes essential for maintaining performance. The rxVirtualFor
directive enables virtual scrolling by rendering only the items currently visible in the viewport. With signal support, you can bind a signal representing a large array to rxVirtualFor
, and it will efficiently manage the rendering process as the user scrolls.
items = signal(['apple', 'banana', 'orange']);
<rx-virtual-scroll-viewport [itemSize]="50">
<div *rxVirtualFor="let item of items">{{ item }}</li>
</rx-virtual-scroll-viewport>
Getting Started with RxAngular and Signals
To start leveraging the power of signals in your RxAngular projects, ensure you have the latest version of RxAngular installed. Then, you can directly use signals with the directives as demonstrated in the examples above.
RxLet supports Subscribable
This was a community contribution, special thanks to Alireza Ebrahimkhani for kickstarting the effort and Adrian Romanski for finishing it off.
Before this version of @rx-angular/template
, the RxLet
directive would only be able to consume ObservableInput
as input values.
On top of the Signal
support, the RxLet
directive now also supports Subscribable
as input which adds yet another layer of convenience.
<div *rxLet="state$; let state">
{{ state }}
</div>
import { RxLet } from '@rx-angular/template/let';
@Component({
imports: [RxLet]
})
export class MyComponent {
state$ = {
subscribe: ({ next }) => {
next(42);
return {
unsubscribe: () => {}
}
}
}
}
The parent flag gets deprecated
Before the introduction of Signal queries (viewChild
, viewChildren
, contentChild
, contentChildren
), the structural directives rxLet
, rxFor
& others had to manually perform a change detection run on their host component whenever the rendering process finished in order to update any open view- or content query.
This behavior was in most cases just causing overrendering, especially if there were no open queries we had to update.
One could already disable this behavior on a per directive basis via the parent
input flag or setting it globally by providing a custom RxRenderStrategiesConfig
.
Disable the parent flag globally
// main.ts
import { RX_RENDER_STRATEGIES_CONFIG } from '@rx-angular/cdk/render-strategies';
bootstrapApplication(AppComponent, {
providers: [
{
provide: RX_RENDER_STRATEGIES_CONFIG,
useValue: {
parent: false // disable parent flag globally
}
}
]
})
Disable the parent flag per directive
<div #state *rxLet="state$; let state; parent: false"></div>
<div #item *rxFor="let item of items$; parent: false"></div>
However, as far as we know, this feature is very little known and not used very often.
What does it mean to disable the parent flag?
When disabling the parent
flag, the @rx-angular/template
directives won't run change detection on their host component. This will improve the performance, but can lead to unwanted side effects, especially if rely on view- or content queries of nodes inserted by our directives.
<div #state *rxLet="state$; let state; parent: false"></div>
It means that any regular @ViewChild
, @ViewChildren
, @ContentChild
& @ContentChildren
are not being updated properly as they rely on the host component to get change detected.
@Component({})
export class QueryComponent {
@ViewChild('state') set stateDiv(div: ElementRef<HTMLElement>) {
// this setter will never be called if there are no
// other triggers for change detection of `QueryComponent`.
}
}
This is also the reason why the default value for this configuration is true
even though it means worse performance in most cases.
Signal queries for the win
Thanks to the recent additions to the angular framework itself, we can now safely get rid of this flag.
The new Signal queries will just work without having us to additionally run change detection on any level. We can just insert nodes into the viewContainer
and the queries will update accordingly.
<div #state *rxLet="state$; let state; parent: false"></div>
It means that any regular @ViewChild
, @ViewChildren
, @ContentChild
& @ContentChildren
are not being updated properly as they rely on the host component to get change detected.
@Component({})
export class QueryComponent {
stateDiv = viewChild<ElementRef<HTMLElement>>('state');
constructor() {
effect(() => {
// this effect will run perfectly fine, even though `rxLet` won't
// run change detection on `QueryComponent`
console.log(this.stateDiv());
})
}
}
RxState functional creation
Thanks to the new inject method and other additions introduced by the angular framework, we were able to re-think the approach on how to create instances of RxState
. The conclusion is that the current way of providing and injecting a token is too cumbersome for local state purposes. Instead we have implemented a functional approach to create new RxState
instances, the RxState
creation function.
See the following example:
import { rxState } from '@rx-angular/state';
@Component()
export class MovieListComponent {
private movieResource = inject(MovieResource);
private state = rxState<{ movies: Movie[] }>(({ set, connect }) => {
// set initial state
set({ movies: [] });
// connect state from resource
connect('movies', this.movieResource.fetchMovies());
});
// select a property for the template to consume as an observable
movies$ = this.state.select('movies');
// OR select a property for the template to consume as a signal
movies = this.state.signal('movies'); // Signal<Movie[]>
}
The instance created by RxState
is tied to the DestroyRef
of the creating host. You don't have to care about unsubscribing or any other form of teardown.
Read more about the new functional approach in our migration guide.
RxState native signal support
We have also introduced native signal support to the RxState
service. As seen in the example above, RxState
is now able to expose values from the state as a Signal
. Here is an overview about the newly added APIs.
import { rxState } from '@rx-angular/state';
import { select } from '@rx-angular/state/selections';
state = rxState<{
movies: Movie[];
search: string;
}>();
// Read a key from state as `Signal`
movieSignal = this.state.signal('movies'); // Signal<Movie[]>
// Derive state from keys and expose as `Signal`
filteredMovies = this.state.computed(({ search, movies }) => {
return movies.filter(movie => movie.title.includes(search))
}); // Signal<Movie[]>
// derive asynchronous filteredMovies from your stored state as a signal
filteredMoviesAsync = this.state.computedFrom(
select('search'),
switchMap((searchValue) => this.movieResource.fetchMovies(searchValue)),
startWith([] as Movie[]), // needed as the initial value otherwise it will throw an error
); // Signal<Movie[]>
Additionally, we've introduced a new overload for connect
, allowing you to also feed your state with any Signal
.
state = rxState<{
movies: Movie[];
search: string;
}>(({ connect }) => {
// Signal<Movie[]>
const moviesAsSignal = this.movieResource.fetchAsSignal();
// connect the signal to the state
connect('movies', moviesAsSignal);
});
You'll find more information about this in the getting started guide.
Custom configuration of RxState with provideRxStateConfig
Users of RxState
are now getting more freedom when it comes to customizing the behavior of RxState
instances.
Let's dive into the details.
provideRxStateConfig
Configurations for RxState
instances are provided in the DI tree by using the provideRxStateConfig
provider function.
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { provideRxStateConfig } from '@rx-angular/state';
bootstrapApplication(AppComponent, {
providers: [
provideRxStateConfig(
/* define features here */
),
],
});
Scheduler
By default, RxState
observes changes and computes new states on the queueScheduler
. You can modify this behavior by using the withScheduler()
or withSyncScheduler()
configuration features.
The queueScheduler provides a certain level of integrity, as state mutations that cause other state mutations are executed in the right order.
"When used without delay, it schedules given task synchronously - executes it right when it is scheduled. However when called recursively, that is when inside the scheduled task, another task is scheduled with queue scheduler, instead of executing immediately as well, that task will be put on a queue and wait for current one to finish."
"This means that when you execute task with queue scheduler, you are sure it will end before any other task scheduled with that scheduler will start."
_src: queueScheduler on rxjs.dev
In conclusion, it is possible that you can run into the situation where a state mutation isn't synchronous.
See the following very simplified example:
import { rxState } from '@rx-angular/state';
const state = rxState<{ foo: string; bar: string }>();
state.set(() => {
// will execute after the { bar: 'bar' } was set
state.set('foo', 'foo');
console.log(state.get('foo')); // prints undefined
// will execute first
return {
bar: 'bar',
};
});
In order to escape this behavior, you can define the scheduling to be fully synchronous:
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { provideRxStateConfig, withSyncScheduler } from '@rx-angular/state';
bootstrapApplication(AppComponent, {
providers: [provideRxStateConfig(withSyncScheduler())],
});
It is however also possible to define whatever SchedulerLike you want, e.g. make your state asynchronous by using the asapScheduler.
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { asapScheduler } from 'rxjs';
import { AppComponent } from './app.component';
import { provideRxStateConfig, withScheduler } from '@rx-angular/state';
bootstrapApplication(AppComponent, {
providers: [
provideRxStateConfig(
/* use the asapScheduler to new states -> makes the state async! */
withScheduler(asapScheduler),
),
],
});
Accumulator
The accumulator defines how state transitions from change to change and how slices are integrated into the state.
By default, RxState
operates immutable on the top level of the state. Deeply nested objects are not shallow cloned on state changes. In order to adjust this behavior or add new functionality, you can define your own AccumulatorFn
. This enables you to e.g. integrate an immerjs based state management.
The AccumulationFn
is a function that runs on every state change. It is responsible for computing a new state from given slices. It's very close to the concept of reducers
. By default it merges together the state by spreading it - producing a new object on every change.
// default-accumulator.ts
import { AccumulationFn } from '@rx-angular/state/selections';
const defaultAccumulator: AccumulationFn = <T>(state: T, slice: Partial<T>): T => {
return { ...state, ...slice };
};
You can now use the withAccumulator
configuration feature to set a custom AccumulatorFn
via the DI tree.
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { provideRxStateConfig, withAccumulator } from '@rx-angular/state';
import { produce } from 'immer';
const immerAccumulator = (state, slice) =>
produce(state, (draft) => {
Object.keys(slice).forEach((k) => {
draft[k] = slice[k];
});
});
bootstrapApplication(AppComponent, {
providers: [
provideRxStateConfig(
withAccumulator(immerAccumulator),
),
],
});
Expose RxState as readOnly
This was a community contribution, special thanks to Adrian Romanski for implementing this feature.
If you only want to expose your RxState
instance as a readonly state, you can use the new asReadOnly()
function. This will expose only APIs that allows consumers to read from your state. Write access remains private to the owner of the RxState instance.
import { inject, Injectable } from '@angular/core';
import { rxState } from '@rx-angular/state';
@Injectable({ providedIn: 'root' })
export class MovieService {
private resource = inject(MovieResource);
private readonly _state = rxState<{ movies: Movie[] }>(({ set, connect }) => {
// set initial state
set({ movies: [] });
// connect global state to your local state
connect('movies', this.resource.fetchMovies());
});
// consumers can use `get`, `select`, `signal` and `computed`
readonly state = this._state.asReadOnly();
}
Thanks for reading this long post and we hope these new features will be useful for you and your team.
If you haven't yet, go and leave a ⭐ for us on github!