68 Funzioni async con Promise all e array map

In questa lezione approfondiamo lo studio di sync await combinandolo con Promise.all.

Riferendoci alla lezione precedente, supponiamo di voler avere i dati dell’utente per ogni album. Aprendo la pagina degli album di json place holder

https://jsonplaceholder.typicode.com/albums

vediamo che abbiamo lo userId, ma non i dati dell’utente come nome e cognome. Riportando il codice precedente per dare continuità:

const albumUrl = 'https://jsonplaceholder.typicode.com/albums';
const photoUrl = 'https://jsonplaceholder.typicode.com/photos';

async function fetchAlbum(id) {
    const album = await fetch(albumUrl + '/' + id).then(resp => resp.json())
        .then(albumcontent => albumcontent);
    const album2 =  fetch(albumUrl + '/' + 2).then(resp => resp.json())
        .then(albumcontent => albumcontent);
    console.log(album2);
    return album;
}

let res = fetchAlbum(1);
res.then( resp => console.log(resp));

aggiungiamo una nuova url che prende gli utenti

const userUrl = 'https://jsonplaceholder.typicode.com/users';

adesso commentando il precedente codice, prendiamo tutti gli albums con una funzione async await

async function fetchAlbums() {
    const albums = await fetch(albumUrl).then(resp => resp.json())
        .then(albumcontent => albumcontent);
    return album;
}

let res = fetchAlbums();
res.then( resp => console.log(resp));

a questo punto abbiamo recuperato tutti gli albums con tutti gli userId, id, title, ma noi vogliamo i dati dello user, quindi dovremo prelevare gli userId di ogni album. L’obbiettivo è far ritornare un oggetto di questo tipo:

return {albums: albums, users: users};

Siccome abbiamo ottenuto gli albums memorizzati nella variabile albums restituiti sottoforma di array, possiamo mappare ciascuno di questi array e fare la chiamata per prendere ciascuno di questi utenti, utilizzando il metodo map

let userPromises = albums.map()

Il metodo map esegue una funzione per ogni elemento dell’array, restituendone l’elemento trasformato dalla funzione. Nel nostro caso restituiremo la fetch dello userId

let userPromises = albums.map(album => {
    return fetch(userUrl + '/' + album.userId );
})

ritorniamo la response col json del metodo then

let userPromises = albums.map(album => {
    return fetch(userUrl + '/' + album.userId ).then( resp => resp.json());
})

a questo punto abbiamo ottenuto un elenco di promeses contenute in userPromises, stampiamole per verifica in console e inizializziamo la costante users come array vuoto per evitare un errore

console.log(userPromises);

const users = [];

a questo punto in console abbiamo un array di promises, ovvero per ogni utente stiamo effettuando una chiamata, cosa da sistemare perchè non dobbiamo fare centinaia di chiamate per recuperare gli utenti, in quanto molti id sono ripetuti.

Per ora risolviamo queste promises in contemporanea con Promise.all passandogli l’array di promeses userPromeses

users = Promise.all(userPromises);

In questo modo sfruttiamo il fatto che queste chiamate verranno effettuate in parallelo e sarà effettuata una chiamata per ogni utente.

Aggiungiamo await davanti, cosìcchè il codice andrà in pausa fintanto che tutte queste promises non saranno risolte, con il vantaggio che le chiamate avverranno in parallelo e non una ad una, come avveniva nel fetch di albumUrl.

users = await Promise.all(userPromises);

Facciamo un console.log di users per vedere i dati restituiti, avremo l’elenco degli utenti con i loro dati e gli albums

console.log(users);

adesso vediamo come migliorare il codice non facendo una chiamata per ogni utente di ogni album, ma solo per gli utenti univoci filtrando l’array degli albums per avere solo essi.

Istanziamo un array vuoto usersArray

let usersArray = [];

Invece che mappare nel modo precedente, usiamo il map su album restituendo solo gli utenti non presenti in questo array. Ragioniamo quindi in questo modo: se l’array usersArray non include album.userId, allora inseriamo questo userId nell’array col metodo push

albums.map( album => {
    if (!usersArray.includes(album.userId)) {
        usersArray.push(album.userId);
    } 
});

Una volta terminato questo map di albums, possiamo ciclare l’array userArray, oppure combiniamo quest’ultima logica con quella precedente, quindi se l’utente non è incluso nell’array temporaneo, allora faccio la chiamata.

1° esempio

let userPromises = albums.map(album => {
    if (!usersArray.includes(album.userId)) {
        usersArray.push(album.userId);
        return fetch(userUrl + '/' + album.userId ).then( resp => resp.json());
    }
})

La volta successiva che il codice passerà, vedrà che l’utente è incluso e non effettuerà la chiamata. Con questa soluzione nella console riceverò in console sempre un array di 100 promeses, molte delle quali sono undefined cosicché dove risulterà undefined non verrà effettuata la chiamata. Infatti spostandoci nel tab network noteremo che le chiamate effettive fatte saranno solo 11, la prima per tirare giù gli albums e altre 10 una per ogni utente diverso, dove il programma incontrerà undefined nell’array, la chiamata fetch non sarà effettuata. In pratica l’array userPromises avrà 100 posizioni al suo interno, ma solo 10 di questo sono delle fetch, gli altri sono valori promises che vengono risolti subito. L’unica parte ancora scorretta è che avremo numerosi posti di utenti inesistenti occupati nell’array. Tale inconsistenza la possiamo risolvere con un filter applicato a users. Rinominiamo la costante users in userAll e applichiamo il filtro dicendo che il tipo è diverso da undefined

const usersAll = await Promise.all(userPromises);
let users = usersAll.filter( val => typeof val != 'undefined');

oppure si può mettere che val è diversa dalla costante undefined ( quindi senza apici )

let users = usersAll.filter( val =>  val != undefined);

otteniamo in entrambi i casi lo stesso risultato, ovvero un array contenete solo 10 promises.

2° esempio

Ripristiniamo il map iniziale di album e se lo userId non c’è lo inserisco col push

let usersArray = [];
albums.map( album => {
    if (!usersArray.includes(album.userId)) {
        usersArray.push(album.userId);
    }
});

a questo punto invece che mappare tutti gli albums come nel 1* esempio, mappo solo userArray

let userPromises = usersArray.map()

Siccome userArray avrebbe l’id degli utenti eseguo la chiamata fetch per ogni userId

let userPromises = usersArray.map(userId => fetch(userUrl + '/' + userId ).then( resp => resp.json()));

Ottenendo lo stesso risultato con un codice nettamente più pulito e corto rispetto al primo esempio, possiamo cancellare la riga del filter inserita nel primo esempio che ormai non serve a nulla

let users = usersAll.filter( val =>  val != undefined);

e lasciare solo

const users = await Promise.all(userPromises);

Questo è un esempio di come combinare async await che mette in pausa il codice con Promise.all che effettua chiamate in parallelo per ogni promises.