Search code examples
javascriptfunctionfetches6-promise

How do I the average of data inside of a fetch request?


So I have a table populated with data from an API. Now I want to add a <p> tag with the average mass key from the data. Here is the code I have to get the table data which works great: const url = 'https://swapi.co/api/species/1/';

  function fetchData(url) {
     return fetch(url).then((resp) => resp.json());
  }


    function constructTableRow(data) {
      const row = document.createElement('tr');
      const { name, height, mass, hair_color } = data;
      row.appendChild(constructElement('td', name))
      row.appendChild(constructElement('td', height))
      row.appendChild(constructElement('td', mass))
      row.appendChild(constructElement('td', hair_color))
      return row;
   }

const swTable = document.getElementById('sw-table').getElementsByTagName('tbody')[0];
fetchData(url).then(data =>
data.people.forEach(personUrl =>
    fetchData(personUrl).then(result => {
      const row = constructTableRow(result);
      swTable.appendChild(row);
   })
 )
);

Now here's the code I have to get the average, which doesn't work:

const link = 'https://swapi.co/api/species/1/';
 function fetchLink(link) {
   return fetch(link).then((resp) => resp.json());
 }

 fetchLink(link).then(data =>
    data.people.forEach(personUrl =>
    fetchData(personUrl).then(result => {
      const getMass = result.mass;
         return getMass.reduce(function(a, b) {
            return a + b;
        }) / getMass.length;
    })
  )

);

When I run this code I get this error:

Uncaught (in promise) TypeError: getMass.reduce is not a function at fetchData.then.result

Can I alter this somehow to run inside of this fetch or do I have to have a separate function?


Solution

  • There are few unhandled problems in your code.

    First, you are trying to call .reduce on each person's mass, which makes little sense:

    const getMass = result.mass;
    return getMass.reduce(function(a, b) {
      return a + b;
    }) / getMass.length;
    

    This is where your getMass.reduce is not a function error comes from – .reduce method works with arrays, and result.mass is, for example "77", so there's no .reduce on it.

    Second, the person's mass is not a number, it's actually a string ("77", not 77), so even an array of these masses (["87", "77", …]) wouldn't get you the sum and average mass:

    ["87", "77"].reduce((a, sum) => a + sum) // -> "8777"
    

    You have to convert these to actual numbers first:

    ["87", "77"].map(a => parseInt(a)) // -> [87, 77]
    [87, 77].reduce((a, sum) => a + sum) // -> 164
    

    Use parseFloat instead of parseInt, if you expect decimal masses (like "77.25").

    Moreover, some of these strings are not even numeric, but "unknown". So you have to filter them out:

    ["87", "77", "unknown"].filter(mass => !isNaN(mass)) // -> ["87", "77"]
    

    This is how i would go about it, hopefully the comments will help you with details:

    const getAvgMass = async url =>
      fetch(url)
        .then(r => r.json())
        .then(
          async data =>
            (await Promise.all( // return the array after all persons are fetched and processed
              data.people.map(personUrl => // take each person's URL,
                  fetch(personUrl) // fetch the data from it,
                    .then(r => r.json())
                    // and replace the URL in an array with person's mass
                    // (parseInt parses numeral strings like "77" to 77 (integer),
                    // and non-numeral strings like "unknown" to NaN):
                    .then(person => parseInt(person.mass)) // => [77, 136, 49, 120, 75, 84, 77, 84, NaN, 80, 77, NaN, …]
              )
            // filter out these NaNs:
            )).filter(mass => !isNaN(mass)) // -> [77, 136, 49, 120, 75, 84, 77, 84, 80, 77, …]
        )
        // sum all masses and divide it by (filtered) array length:
        .then(masses => masses.reduce((sum, x) => sum + x) / masses.length); // -> 82.77272…
    
    
    // top-level await is not (yet? https://github.com/MylesBorins/proposal-top-level-await) supported
    // in browsers (except Chrome console in recent versions), so to log the result, we have to do:
    // getAvgMass("https://swapi.co/api/species/1/").then(result => console.log(result)); // logs 82.77272…
    
    // or:
    // const logResult = async () => console.log(await getAvgMass("https://swapi.co/api/species/1/"));
    // logResult(); // logs 82.77272…
    
    // or even:
    // (async () => {
    //   console.log(await getAvgMass("https://swapi.co/api/species/1/")) // logs 82.77272…
    // })();
    
    // to use in a DOM element, just replace console.log:
    
    (async () => {
      const avgMass = await getAvgMass("https://swapi.co/api/species/1/");
      console.log(avgMass); // logs 82.77272…
      document.getElementById("sw-mass").innerText = avgMass.toFixed(2); // sets the <span> text to 82.77
    })();
    <p>average mass: <span id="sw-mass">…</span></p>