DEV Community

Cover image for The next improvement in Angular reactivity
Nicolas Frizzarin for This is Angular

Posted on

The next improvement in Angular reactivity

Introduction

Since the latest versions of Angular, a new primitive reactivity system has been developed within the framework: signals!

Today, with hindsight, we realize that certain use cases had not been covered, and obviously the Angular team being very reactive will provide us with helpers to cover these uses cases.

What are these uses cases? What solutions are going to be put in place, and how are they going to be used?

Process of resetting one signal relative to another

Let's start by illustrating this problem.

Let's imagine we have a basket of fruit with a certain quantity.
The quantity is managed by a component, which inputs the fruit.

@Component({
  template: `<button type="button" (click)="updateQuantity()"> 
    {{quantity()}}
    </button>`
})
export class QuantityComponent() {
  fruit = input.required<string>();
  count = signal(1);

  updateQuantity(): void {
    this.count.update(prevCount => prevCount++);
  }
}
Enter fullscreen mode Exit fullscreen mode

Here the variable must be reset if the input price fruit changes.

A simple solution would be to use an effect

@Component({
  template: `<button type="button" (click)="updateQuantity()"> 
    {{quantity()}}
    </button>`
})
export class QuantityComponent() {
  fruit = input.required<string>();
  quantity = signal(1);

  countEffect(() => {
    this.fruit();
    this.quantity.set(1);
  }, { allowSignalWrites: true })

  updateQuantity(): void {
    this.quantity.update(prevCount => prevCount++);
  }
}
Enter fullscreen mode Exit fullscreen mode

The preceding code is bad practice. Why is this the big question?

We need to set the signalWrites option to true in order to set the signal quantity. This is due to a misinterpretation of the given problem.
In our case, we want to synchronize two variables which, in our materialization, are desynchronized

The counter is not independent of the fruit, which is our initial source. In reality, here we have a component state, whose initial source is the fruit and the rest is a derivative of the fruit.

Materialize the problem as follows

@Component({
  template: `<button type="button" (click)="updateQuantity()"> 
    {{fruitState().quantity()}}
    </button>`
})
export class QuantityComponent() {
  fruit = input.required<string>();

  fruitState = computed(() => ({
    source: fruit(),
    quantity: signal(1),
  }));

  updateQuantity(): void {
    this.fruitState().quantity.update(prevCount => prevCount++);
  }
}
Enter fullscreen mode Exit fullscreen mode

This materialization strongly links the fruit to its quantity.
So as soon as the fruit changes, the computed variable fruitState is automatically recalculated. This recalculation returns an object with the quantity property, which is a signal initialized to 1.

By returning a signal, the variable can be incremented on click and simply reset when the fruit changes.

It's a relatively simple pattern to set up, but can't we simplify it?

The LinkedSignal function to the rescue.

With the arrival of Angular 19, a new function for calculating derived signals.

Until now, we had the computed function, but this function returns a Signal and not a WrittableSignal, which would have been practical in our previous use case for the quantity variable.

This is where LinkedSignal comes in. LinkedSignal, as its name suggests, allows you to strongly link two signals together.

If we return to our previous case, this function would allow us to simplify the code as follows:

@Component({
  template: `<button type="button" (click)="updateQuantity()"> 
    {{quantity()}}
    </button>`
})
export class QuantityComponent() {
  fruit = input.required<string>();
  quantity = linkedSignal({ source: fruit, computation: () => 1 });

  updateQuantity(): void {
    this.quantity.update(prevCount => prevCount++);
  }
}
Enter fullscreen mode Exit fullscreen mode

The linkedSignal function is defined as follows:

linkedSignal(computation: () => D, options?: { equal?: ValueEqualityFn<NoInfer<D>>; }): WritableSignal<D>;

linkedSignal(options: { source: () => S; computation: (source: NoInfer<S>, previous?: { source: NoInfer<S>; value: NoInfer<D>; }) => D; equal?: ValueEqualityFn<NoInfer<D>>; }): WritableSignal<D>;
Enter fullscreen mode Exit fullscreen mode

In the first definition, the “abbreviated” definition, the linkedSignal function takes a computation function as a parameter and a config object.

const quantity = input.required<number>();
const price = linkedSignal(() => quantity() * 0);
Enter fullscreen mode Exit fullscreen mode

In this previous exemple, because the computation function depends of the quantity sigal, when the quantity change, the computation function is reevaluated.

In the second definition, the linkedFunction method takes an object as a parameter with three properties

  • the source: the signal on which the computation function bases its re-evaluation
  • the computation function
  • a parameter object

Contrary to the “abbreviated” computation function, here the computation function takes as parameters the value of the source and a “precedent”.

const fruits = signal(['apple', 'orange']);
const choice = linkedSignal({
  source: fruits,
  computation: (source, previous) => {
    if(!Boolean(previous)) {
      return 'apple';
    }
    return previous.source.find(fruit => fruit === previous.value) ||   
      'apple';
  }
})
Enter fullscreen mode Exit fullscreen mode

The new Resource Api

Angular 19 will introduce a new API for simple data fetching and retrieval of query status (pending etc), data and errors.

For those who are a little familiar with the framework, this new API works a bit like the useRessource hook.

Let's take a look at an example:


import { resource } from "@angular/core";

@Component()
export class FruitComponent {
  fruitId = input.required<string>();
  fruitRessource = resource({
    loader: () => {
      return fetch(`https://myFruit.com/${this.fruitId()}`).then(response => 
      response.json());
    },
  });

  fruitRessourceEffect(() => {
    console.log("Status: ", this.todoResource.status());
    console.log("Value: ", this.todoResource.value());
    console.log("Error: ", this.todoResource.error());
  })
}
Enter fullscreen mode Exit fullscreen mode

There are several things to know about this code snippet

There are several things to note in this snippet code:

  • by default, the loader takes a promise as parameter
  • the fruitRessource type is a WrittableRessource, which will enable us to modify the data locally if required
  • the request for fruit details is sent directly to the server when the resource fuitResource is created
  • this.fruitId() is untracked in the loader, so no new request will be send if the fruitId change
  • WrittableRessource have a method refresh to refresh the data

The following effect will print those value

Status: 'pending'
Value: undefined,
Error: undefined,

// When the promise is resolved, we enter again in the effect, because the status and value are signals and they changed

Status: 'resolved'
Value: { name: 'apple', count: 2 },
Error: undefined,
Enter fullscreen mode Exit fullscreen mode

as explained above, by default the fruitId signal is untracked.

So how do you restart the http request each time the value of this signal changes, but also how do you cancel the previous request in the event that the value of the fruitId signal changes and the response to the previous request didn't arrive?

The resource function takes another property called request.

This property takes as its value a function that depends on signals and returns their value.


import { resource } from "@angular/core";

@Component()
export class FruitComponent {
  fruitId = input.required<string>();
  fruitRessource = resource({
    request: this.fruitId
    loader: (request, abortSignal) => {
      return fetch(`https://myFruit.com/${request}`}, { signal: abortSignal }).then(response => 
      response.json());
    },
  });

  fruitRessourceEffect(() => {
    console.log("Status: ", this.todoResource.status());
    console.log("Value: ", this.todoResource.value());
    console.log("Error: ", this.todoResource.error());
  })
}
Enter fullscreen mode Exit fullscreen mode

As shown in the code above, the loader function takes two parameters

  • the request value: here the value of the fruitId signal
  • the value of the signal property of the abortController object used to cancel an http request.

So if the value of the fruitId signal changes during an httpRequest retrieving the details of a fruit, the request will be canceled to launch a new request.

Finally, Angular has also thought of the possibility of coupling this new api with RxJs, allowing us to benefit from the power of Rx operators.

Interporability is achieved using the rxResource function, which is defined in exactly the same way as the resource function.
The only difference will be the return type of the loader property, which will return an observable


import { resource } from "@angular/core";

@Component()
export class FruitComponent {
  fruitId = input.required<string>();
  fruitRessource = rxResource({
    request: this.fruitId
    loader: request => {
      return this.httpClient.get<any>(`https://myFruit.com/${request}`)
    }
  });

  fruitRessourceEffect(() => {
    console.log("Status: ", this.todoResource.status());
    console.log("Value: ", this.todoResource.value());
    console.log("Error: ", this.todoResource.error());
  })
}
Enter fullscreen mode Exit fullscreen mode

Here it's not necessary to have the abortSignal, the cancelling of the previous request when the value of the signal fruitId change is implicit in the function rxResource and the behaviour will be the same as the switchMap operator.

Top comments (0)