
Il rilascio di Angular v19, avvenuto poche settimane fa, segna un traguardo importante nella rivoluzione dei signal all’interno del framework, con le API Input, Model, Output e Signal Queries ora ufficialmente promosse a stabili.
Ma non è tutto! Questa versione major introduce anche nuovi strumenti potenti progettati per far progredire ulteriormente la rivoluzione dei signal: la nuova Resource API.
Come suggerisce il nome, questa nuova Resource API è progettata per semplificare il caricamento di risorse asincrone sfruttando tutta la potenza dei signal!
IMPORTANTE: al momento della scrittura, la nuova Resource API è ancora in fase sperimentale. Ciò significa che potrebbe subire modifiche prima di diventare stabile, quindi usatela a vostro rischio. 😅
Vediamo insieme in che modo ci può semplificare la gestione delle risorse asincrone!
La nuova Resource API
La maggior parte delle signal API sono sincrone, ma nelle applicazioni del mondo reale è fondamentale gestire risorse asincrone, come il recupero dei dati da un server o la gestione delle interazioni dell’utente in tempo reale.
È qui che entra in gioco la nuova Resource API.
Utilizzando una Resource
, è possibile consumare facilmente una risorsa asincrona tramite i signal, consentendo di gestire facilmente il recupero dei dati, gli stati di caricamento e far partire una nuova richiesta ogni volta che i parametri del signal associato cambiano.
La funzione resource( )
Il modo più semplice per creare una Resource
è utilizzare la funzione resource()
:
import { resource, signal } from '@angular/core';
const RESOURCE_URL = 'https://jsonplaceholder.typicode.com/todos/';
private id = signal(1);
private myResource = resource({
request: () => ({ id: this.id() }),
loader: ({ request }) => fetch(RESOURCE_URL + request.id),
});
Code language: TypeScript (typescript)
Questa funzione accetta un oggetto di configurazione ResourceOptions
come input, permettendoci di specificare le seguenti proprietà:
request
: una funzione reattiva che determina i parametri utilizzati per eseguire la richiesta alla risorsa asincrona;loader
: una funzione di caricamento che restituisce unaPromise
del valore della risorsa, può essere basata sui parametri forniti dalla funzionerequest
.
Questa è l’unica proprietà obbligatoria diResourceOptions
;equal
: una funzione di uguaglianza utilizzata per confrontare il valore restituito dalla funzioneloader
;injector
: sovrascrive l’Injector
utilizzato dall’istanza diResource
per distruggersi quando il componente o il servizio genitore viene distrutto.
Grazie a queste configurazioni, possiamo definire facilmente una dipendenza asincrona che verrà consumata in modo efficiente e mantenuta sempre aggiornata.
Ciclo vita di una Resource
Una volta creata una Resource
, la funzione
viene eseguita, per poi far partire la richiesta asincrona risultante:loader
import { resource, signal } from "@angular/core";
const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/";
const id = signal(1);
const myResource = resource({
request: () => ({ id: id() }),
loader: ({ request }) => fetch(RESOURCE_URL + request.id)
});
console.log(myResource.status()); // Stampa: 2 (che significa "Loading")
Code language: TypeScript (typescript)
Ogni volta che un signal da cui dipende la funzione request
cambia, la funzione request
viene eseguita di nuovo e, se restituisce nuovi parametri, la funzione
viene eseguita per recuperare il valore aggiornato della risorsa.loader
import { resource, signal } from "@angular/core";
const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/";
const id = signal(1);
const myResource = resource({
request: () => ({ id: id() }),
loader: ({ request }) => fetch(RESOURCE_URL + request.id)
});
console.log(myResource.status()); // Stampa: 2 (che significa "Loading")
// Dopo che la fetch è stata completata
console.log(myResource.status()); // Stampa: 4 (che significa "Resolved")
console.log(myResource.value()); // Stampa: { "id": 1 , ... }
id.set(2); // Scatena una nuova richiesta, causando l'esecuzione della funzione loader di nuovo
console.log(myResource.status()); // Stampa: 2 (che significa "Loading")
// Dopo che la fetch è stata completata
console.log(myResource.status()); // Stampa: 4 (che significa "Resolved")
console.log(myResource.value()); // Stampa: { "id": 2 , ... }
Code language: TypeScript (typescript)
Se non viene fornita una funzione request
, la funzione
verrà eseguita solo una volta, a meno che la loader
Resource
non venga ricaricata utilizzando il metodo
(ne parleremo più avanti).reload
Infine, una volta che il componente o il servizio genitore viene distrutto, anche la Resource
viene distrutta, a meno che non sia stato fornito un injector
specifico.
In questi casi, la Resource
rimarrà attiva e verrà distrutta solo quando l’injector
fornito verrà distrutto a sua volta.
Annullare le richieste con abortSignal
Per ottimizzare il recupero dei dati, una Resource
può annullare le richieste in sospeso se i valori restituiti della funzione request()
cambiano, mentre un valore precedente è ancora in fase di caricamento.
Per gestire questi casi, la funzione loader()
fornisce un abortSignal
, che possiamo passare alle nostre richieste, come fetch
. La funzione resta in ascolto dell’
e annulla l’operazione se questo emette un segnale, garantendo una gestione efficiente delle risorse e prevenendo richieste di rete inutili:abortSignal
import { resource, signal } from "@angular/core";
const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/";
const id = signal(1);
const myResource = resource({
request: () => ({ id: id() }),
loader: ({ request, abortSignal }) =>
fetch(RESOURCE_URL + request.id, { signal: abortSignal })
});
console.log(myResource.status()); // Stampa: 2 (che significa "Loading")
// Scatena una nuova richiesta, facendo interrompere la precedente richiesta
// Poi la funzione loader viene eseguita di nuovo, generando una nuova richiesta di fetch
id.set(2);
console.log(myResource.status()); // Stampa: 2 (che significa "Loading")
Code language: TypeScript (typescript)
Sulla base di ciò, è consigliato utilizzare la Resource API principalmente per le richieste GET, poiché sono generalmente sicure e possono essere annullate senza causare problemi.
Per le richieste POST o UPDATE, annullarle potrebbe causare effetti collaterali indesiderati, come l’invio incompleto dei dati o aggiornamenti non riusciti. Tuttavia, quando abbiamo bisogno di una funzionalità simile per questi tipi di richieste, possiamo utilizzare il metodo effect()
per gestire in modo sicuro le operazioni.
Come consumare una Resource
La Resource API fornisce diverse proprietà sotto forma di signal per leggere il suo stato, che possiamo facilmente utilizzare direttamente all’interno dei nostri componenti o servizi:
value
: contiene il valore corrente dellaResource
, oundefined
se non è disponibile alcun valore. Essendo unWritableSignal
, può essere aggiornato manualmente;status
: contiene lo stato attuale dellaResource
, indica cosa sta facendo laResource
e cosa ci si può aspettare dal suo valore;error
: se si trova nello stato di errore, contiene l’errore più recente sollevato durante il caricamento dellaResource
;isLoading
: indica se laResource
sta caricando un nuovo valore o ricaricando quello esistente.
Ecco un esempio di come consumare una Resource
all’interno di un componente:
import { Component, resource, signal } from '@angular/core';
const BASE_URL = 'https://jsonplaceholder.typicode.com/todos/';
@Component({
selector: 'my-component',
template: `
@if (myResource.value()) {
{{ myResource.value().title }}
}
<button (click)="fetchNext()">Carica il prossimo elemento</button>
`
})
export class MyComponent {
private id = signal(1);
protected myResource = resource({
request: () => ({ id: this.id() }),
loader: ({ request }) =>
fetch(BASE_URL + request.id).then((response) => response.json()),
});
protected fetchNext(): void {
this.id.update((id) => id + 1);
}
}
Code language: TypeScript (typescript)
In questo esempio, la Resource
viene utilizzata per recuperare i dati da un’API in base al valore del signal
, che può essere incrementato cliccando un pulsante.id
Ogni volta che l’utente clicca il pulsante, il valore del signal
cambia, attivando la funzione id
per recuperare un nuovo elemento dall’API remota.loader
L’interfaccia utente si aggiorna automaticamente con i dati recuperati grazie alle proprietà di signal esposte dalla Resource
API.
Leggere lo stato di una Resource
Come accennato in precedenza, il signal status
fornisce informazioni sullo stato attuale della risorsa in un dato momento.
I possibili valori del signal status
sono definiti dall’enum ResourceStatus
. Ecco un riepilogo di questi stati e dei loro valori corrispondenti:
- Idle =
0
: laResource
non ha una richiesta valida e non eseguirà alcun caricamento.value()
è
;undefined
- Error =
1
: il caricamento è fallito con un errore.value()
èundefined
; - Loading =
2
: la risorsa sta attualmente caricando un nuovo valore a seguito di una modifica nella sua richiesta.value()
èundefined
; - Reloading =
3
: la risorsa sta attualmente ricaricando un valore aggiornato per la stessa richiesta.value()
continuerà a restituire il valore precedentemente recuperato fino al completamento dell’operazione di ricaricamento; - Resolved =
4
: il caricamento è completato.value()
contiene il valore restituito dal processo di recupero dei dati del risultato della funzioneloader()
; - Local =
5
: il valore è stato impostato localmente tramiteset()
oupdate()
.value()
contiene il valore assegnato manualmente.
Questi stati aiutano a tracciare i progressi della Resource
e facilitano una gestione migliore delle operazioni asincrone nelle nostre applicazioni.
La funzione hasValue( )
Data la complessità di questi stati, la Resource API fornisce un metodo hasValue()
, che restituisce un valore booleano in base allo stato attuale.
Questo garantisce informazioni accurate sullo stato della Resource
, fornendo un modo più affidabile per gestire le operazioni asincrone senza fare affidamento sul valore, che potrebbe essere
in determinati stati.undefined
hasValue() {
return (
this.status() === ResourceStatus.Resolved ||
this.status() === ResourceStatus.Local ||
this.status() === ResourceStatus.Reloading
);
}
Code language: TypeScript (typescript)
Questo metodo è reattivo, permettendoti di consumarlo e tracciarlo come un signal.
La funzione isLoading( )
La Resource API fornisce anche un signal
, che restituisce se la risorsa è attualmente nello stato di isLoading
Loading
o Reloading
:
readonly isLoading = computed(
() =>
this.status() === ResourceStatus.Loading ||
this.status() === ResourceStatus.Reloading
);
Code language: TypeScript (typescript)
Dato che isLoading
è un computed signal, può essere tracciato reattivamente, permettendoci di monitorare lo stato in tempo reale utilizzando le API dei signal.
value come WritableSignal
Il signal value
fornito da una Resource
è un
, che ci consente di aggiornarlo manualmente utilizzando le funzioni WritableSignal
set()
e update()
:
import { resource, signal } from "@angular/core";
const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/";
const id = signal(1);
const myResource = resource({
request: () => ({ id: id() }),
loader: ({ request }) => fetch(RESOURCE_URL + request.id)
});
console.log(myResource.status()); // Stampa: 2 (che significa "Loading")
// Dopo che la fetch è stata completata
console.log(myResource.status()); // Stampa: 4 (che significa "Resolved")
console.log(myResource.value()); // Stampa: { "id": 1 , ... }
myResource.value.set({ id: 2 });
console.log(myResource.value()); // Stampa: { id: 2 }
console.log(myResource.status()); // Stampa: 5 (che significa "Local")
myResource.value.update((value) => ({ ...value, name: 'Davide' });
console.log(myResource.value()); // Stampa: { id: 2, name: 'Davide' }
console.log(myResource.status()); // Stampa: 5 (che significa "Local")
Code language: TypeScript (typescript)
Nota: come possiamo vedere, l’aggiornamento manuale del valore del signal imposterà anche lo stato su 5, che significa “Local“, per indicare che il valore è stato impostato localmente.
Il valore impostato manualmente persisterà fino a quando non verrà impostato un nuovo valore o non verrà eseguita una nuova richiesta, che lo sovrascriverà con un nuovo valore.
import { resource, signal } from "@angular/core";
const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/";
const id = signal(1);
const myResource = resource({
request: () => ({ id: id() }),
loader: ({ request }) => fetch(RESOURCE_URL + request.id)
});
console.log(myResource.status()); // Stampa: 2 (che significa "Loading")
// Dopo che la fetch è stata completata
console.log(myResource.status()); // Stampa: 4 (che significa "Resolved")
console.log(myResource.value()); // Stampa: { "id": 1 , ... }
myResource.value.set({ id: 2 });
console.log(myResource.value()); // Stampa: { id: 2 }
console.log(myResource.status()); // Stampa: 5 (che significa "Local")
id.set(3); // Triggers a request, causing the loader function to run again
console.log(myResource.status()); // Stampa: 2 (che significa "Loading")
// Dopo che la fetch è stata completata
console.log(myResource.status()); // Stampa: 4 (che significa "Resolved")
console.log(myResource.value()); // Stampa: { "id": 3 , ... }
Code language: TypeScript (typescript)
Nota: il signal
value
della Resource API utilizza lo stesso pattern della nuovaLinkedSignal
API, ma non la utilizza internamente. 🤓
Metodi wrapper di convenienza
Per semplificare l’uso del signal
, la Resource API fornisce wrapper di convenienza per i metodi value
set
, update
e asReadonly
.
Il metodo
è particolarmente utile poiché restituisce un’istanza di sola lettura del signal asReadonly
, consentendo soltanto la lettura e prevenendo modifiche accidentali.value
Possiamo utilizzare questo approccio per creare servizi che gestiscono e tracciano i cambiamenti dei valori delle risorse esportando un’istanza di sola lettura del signal value
:
import { resource, signal } from "@angular/core";
const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/";
export class MyService {
const id = signal(1);
const myResource = resource({
request: () => ({ id: id() }),
loader: ({ request }) => fetch(RESOURCE_URL + request.id })
});
public myValue = myResource.value.asReadonly();
setValue(newValue) {
// Wrapper di `myResource.value.set()`
myResource.set(newValue);
}
addToValue(addToValue) {
// Wrapper di `myResource.value.update()`
myResource.update((value) => ({ ...value, ...addToValue });
}
}
// Utilizzo del servizio in un componente o in un'altra parte dell'applicazione
const myService = new MyService();
myService.myValue.set(null); // Property 'set' does not exist in type 'Signal'
myService.setValue({ id: 2 });
console.log(myService.myValue()); // Stampa: { id: 2 }
myService.addToValue({ name: 'Davide' });
console.log(myService.myValue()); // Stampa: { id: 2, name: 'Davide' }
Code language: TypeScript (typescript)
Questo impedirà ai consumatori di modificare il valore, riducendo il rischio di modifiche non intenzionali e migliorando la coerenza nella gestione dei dati complessi.
Ricaricare o distruggere una Resource
Quando si lavora con risorse asincrone, possiamo trovarci di fronte a scenari in cui è necessario aggiornare i dati o distruggere la Resource
.
Per gestire questi scenari, la Resource API fornisce due metodi dedicati che offrono soluzioni efficienti per la gestione di queste azioni.
La funzione reload( )
Il metodo reload()
istruisce la Resource
a rieseguire la richiesta asincrona, assicurandosi di recuperare i dati più aggiornati:
import { resource, signal } from "@angular/core";
const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/";
const id = signal(1);
const myResource = resource({
request: () => ({ id: id() }),
loader: ({ request }) => fetch(RESOURCE_URL + request.id)
});
console.log(myResource.status()); // Stampa: 2 (che significa "Loading")
// Dopo che la fetch è stata completata
console.log(myResource.status()); // Stampa: 4 (che significa "Resolved")
myResource.reload(); // Returns true if a reload was initiated
console.log(myResource.status()); // Stampa: 3 (che significa "Reloading")
// Dopo che la fetch è stata completata
console.log(myResource.status()); // Prints: 5 (che significa "Local")
Code language: TypeScript (typescript)
Il metodo reload()
restituisce true
se il nuovo caricamento è stato avviato con successo.
Se un ricaricamento non può essere eseguito, sia perché non è necessario, ad esempio quando lo stato è già Loading o Reloading, o non supportato, come quando lo stato è Idle, il metodo restituisce false
.
La funzione destroy( )
Il metodo destroy()
distrugge manualmente la Resource
, distruggendo qualsiasi effect()
utilizzato per tracciare i cambiamenti della richiesta, annullando eventuali richieste in sospeso e impostando lo stato su Idle
, mentre resetta il valore a undefined
.
import { resource, signal } from "@angular/core";
const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/";
const id = signal(1);
const myResource = resource({
request: () => ({ id: id() }),
loader: ({ request }) => fetch(RESOURCE_URL + request.id)
});
console.log(myResource.status()); // Stampa: 2 (che significa "Loading")
// Dopo che la fetch è stata completata
console.log(myResource.status()); // Stampa: 4 (che significa "Resolved")
myResource.destroy(); // Restituisce true se l'operazione di reload è stata avviata
console.log(myResource.status()); // Stampa: 1 (che significa "Idle")
console.log(myResource.value()); // Stampa: undefined
Code language: TypeScript (typescript)
Dopo che una Resource
è stata distrutta, non risponderà più ai cambiamenti della richiesta o alle operazioni reload()
.
Nota: a questo punto, mentre il signal
rimane scrivibile, la
value
Resource
perderà il suo scopo originale e non svolgerà più la sua funzione, diventando praticamente inutile. 🙃
La funzione rxResource( )
Come quasi tutte le API basate su signal introdotte finora, la Resource
API offre anche un’utilità di interoperabilità per un’integrazione con RxJS.
Invece di utilizzare il metodo resource()
per creare una Resource
basata su Promise, possiamo usare il metodo rxResource()
per utilizzare gli Observable:
import { resource, signal } from "@angular/core";
import { rxResource } from '@angular/core/rxjs-interop';
import { fromFetch } from 'rxjs/fetch';
const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/";
const id = signal(1);
const myResource = rxResource({
request: () => ({ id: id() }),
loader: ({ request }) => fromFetch(RESOURCE_URL + request.id)
});
console.log(myResource.status()); // Stampa: 2 (che significa "Loading")
// Dopo che la fetch è stata completata
console.log(myResource.status()); // Stampa: 4 (che significa "Resolved")
console.log(myResource.value()); // Stampa: { "id": 1 , ... }
Code language: TypeScript (typescript)
Nota: il metodo
rxResource()
è infatti esposto dal pacchettorxjs-interop
.
Dall’Observable prodotto dalla funzione loader()
verrà letto solo il primo valore emesso, le emissioni successive saranno ignorate.
Grazie per aver letto questo articolo 🙏
Mi piacerebbe avere qualche feedback quindi grazie in anticipo per qualsiasi commento. 👏
Infine, se ti è piaciuto davvero tanto, condividilo con la tua community. 👋😁