2

Goal: I want to build a class that addresses a news interface and renders the data in the view. My class (MyNews) has several methods and one method is fetchNews(). In this method I get the data from the API. The data from the api should be assigned in a class varaible (news). In another method (showNews()) I want to access the data to iterate over it.

My problem: I am beginner in js and I wonder how to query the data. From my understanding the flow is like this: constructor calls fetchNews fetch news first gets a promise and waits until the promise is resolved then the data is assigned to the member but the JS async is running js doesn't wait at this point until the promise is resolved and it continues working with the empty array. This is exactly my problem.

Question What do I need to do or change to fetch the data correctly. So that I can build a HTML list for example.

My JS Code

class MyNews {
  news = [];
  
  apiUrl = "https://jsonplaceholder.typicode.com/posts";
  
  constructor() {
    this.news;
    this.fetchNews();
  }
  
  fetchNews() {
    fetch(this.apiUrl)
      .then(async (data) => {
        this.news = await data.json(); 
        console.log("A:",this.news.length); // i have results
      })
      .catch((err) => {
        console.log(err)
      });
                // ...
  }
  
  showNews() {
    console.log("B:",this.news.length); // empty array
    
    return this.news;
  }
}
    

n = new MyNews;
console.log("C:", n.showNews().length );
Max Pattern
  • 1,430
  • 7
  • 18

5 Answers5

3

A couple of issues:

  1. The first is that fetchNews is an async process. In your example showNews will always be 0 because fetchNews hasn't fetched anything when showNews is called. We can mitigate some of this by moving the calling of those methods outside of the class.

  2. You have an odd mix of fetch and async/await - it's best to stick with one or the other.

class MyNews {

  apiUrl = 'https://jsonplaceholder.typicode.com/posts';

  // `async/await` function - fetches the JSON, and
  // sets the `news` field to the parsed JSON
  async fetchNews() {
    try {
      const response = await fetch(this.apiUrl);
      if (response.ok) this.news = await response.json();
    } catch (err) {
      this.error = err.message;
    }
  }
  
  formatNews() {
    return this.news.map(n => {
      return `<p>${n.title}</p>`;
    }).join('');
  }
  
  // Return the news or the error if there was one.
  showNews() {
    if (!Array.isArray(this.news)) return this.error;
    return this.formatNews();
  }

}

// Piecing it together we create a new
// object, call and `await` `fetchNews` and
// then call `showNews`
async function main() {
  const myNews = new MyNews();
  await myNews.fetchNews();
  document.body.innerHTML = myNews.showNews();
}


main();
p { border: 1px solid #787878; padding: 0.3em; border-radius: 5px; margin: 0.2em; text-transform: capitalize; }
p:hover { cursor: pointer; background-color: #ffffd0; }
Andy
  • 61,948
  • 13
  • 68
  • 95
2

The problem is that fetch() is an async function. This is what actually happens:

  1. Constructor is called
  2. fetchNews is started
  3. Constructor probably finishes before fetchNews
  4. showNews probably runs before fetchNews is done

First thing that comes to mind is using the await keyword or using promises directly. This does not fully solve your problem though, because constructor can not be an async function.

I recommend this solution:

Move the call of fetchNews to an init method on the MyNews class and make it async.

class MyNews {
    ...
    async fetchNews() {
        await fetch(...);
    }
    ...
    async init() {
        await fetchNews();
    }
    ...
}
const news = new MyNews();
await news.init();
news.showNews();

You can find more on async constructors in this stackoverflow thread.

btw: the this.news; in the constructor seems unnecessary.

Patrik
  • 107
  • 1
  • 7
1

I'd suggest adding a getAndShowNews() function to your class.

We'll then ensure to return the promise from the fetchNews() fetch call, so we can await it in another async function.

In the getAndShowNews() function we'll await the fetchNews() result (a promise), this will ensure the news items are populated. We'll then show the news by looping through each item.

You could do this in a function outside of the class, but it seems to make sense to do it in the class.

class MyNews {
  news = [];
  
  apiUrl = "https://jsonplaceholder.typicode.com/posts";
  
  constructor() {
    this.fetchNews();
  }
  
  fetchNews() {
    return fetch(this.apiUrl)
      .then(async (data) => {
        this.news = await data.json(); 
      })
      .catch((err) => {
        console.log(err)
      });        // ...
  }
  
  showNews(newsElementId, count = 8) {
    let newsElement = document.getElementById(newsElementId);
    for(let newsItem of this.news.slice(0,count)) {
      let el = document.createElement('li');
      el.innerText = newsItem.title;
      newsElement.appendChild(el);
    }
  }

  async getAndShowNews(newsElementId) {
    // Wait for the fetchNews() call to complete...
    await this.fetchNews();
    // Show the news...
    this.showNews(newsElementId);
  }
}
    
n = new MyNews();
n.getAndShowNews('news');
<b>News</b>
<ol id='news'>
</ol>
Terry Lennox
  • 29,471
  • 5
  • 28
  • 40
0

The constructor's call to fetchNews does NOT waits for it. It just creates a Promise (e.g a handle) to something that'll be resolved later. Your showNews needs to be chained after the fetchNews. Also, I would write it with async/await for clarity

class MyNews {
  news = [];
  
  apiUrl = "https://jsonplaceholder.typicode.com/posts";
  
  constructor() {
    // this.news; - This statement does nothing
    // this.fetchNews(); - No need to call in constructor
  }
  
  fetchNews() {
    try {
      const data = await fetch(this.url)
      this.news = await data.json()
      console.log("A:",this.news.length); // i have results
    } catch (err) {
      console.log(err)
    }
  }
  
  showNews() {
    console.log("B:",this.news.length); // empty array
    
    return this.news;
  }
}
    

n = new MyNews();
n.fetchNews().then( () => n.showNews())
// OR
await n.fetchNews()
n.showNews()

I would also return the news from fetchNews instead of keep as a class member

mati.o
  • 1,398
  • 1
  • 13
  • 23
0

Is that OK for you ?

class MyNews {
  
  apiUrl = "https://jsonplaceholder.typicode.com/posts";
  
  constructor() {
    this.news=[];
  }
  
  async fetchNews() {
  const response= await  fetch(this.apiUrl);
  const data = await response.json();
  this.news= data;
  console.log("A:",this.news.length);
  return data;
  }
  
  async showNews() {
    await this.fetchNews();
    console.log("B:",this.news.length); // empty array
    return this.news;
  }
}
    
async function app(){
  n = new MyNews();
  const news= await n.showNews();
  console.log("C:", news.length);
}
app();
   
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jun 08 '22 at 12:51