Netidee Blog Bild
New Features for RxAngular
Native signal support & improved state configuration (27.05.2024)
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: rxIfrxLetrxFor, 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.

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

 

<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 (viewChildviewChildrencontentChildcontentChildren), the structural directives rxLetrxFor & 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!

CAPTCHA
Diese Frage dient der Überprüfung, ob Sie ein menschlicher Besucher sind und um automatisierten SPAM zu verhindern.
    Datenschutzinformation
    Der datenschutzrechtliche Verantwortliche (Internet Privatstiftung Austria - Internet Foundation Austria, Österreich) würde gerne mit folgenden Diensten Ihre personenbezogenen Daten verarbeiten. Zur Personalisierung können Technologien wie Cookies, LocalStorage usw. verwendet werden. Dies ist für die Nutzung der Website nicht notwendig, ermöglicht aber eine noch engere Interaktion mit Ihnen. Falls gewünscht, können Sie Ihre Einwilligung jederzeit via unserer Datenschutzerklärung anpassen oder widerrufen.