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.

Intermediate workshop

Modern Angular

Master the latest Angular features to build modern applications. Learn how to use standalone components, signals, the new inject method and much more.

Modern Angular
Angular Logo
Angular Logo - Dependency Injection
Angular Logo - Performance
Angular Logo - Forms

Advanced workshop

Angular Performance

High-Speed Angular applications on any device

Angular Performance
Angular Logo
Google Chrome icon - Logo

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.

๐Ÿ“ƒ Read more in the docs

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' });

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']);

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']);

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.

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

Disable the parent flag per directive

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.

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.

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:

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.

Additionally, we've introduced a new overload for connect, allowing you to also feed your state with any Signal.

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.

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:

In order to escape this behavior, you can define the scheduling to be fully synchronous:

It is however also possible to define whatever SchedulerLike you want, e.g. make your state asynchronous by using the 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.

You can now use the withAccumulator configuration feature to set a custom AccumulatorFn via the DI tree.

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.

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!