user2030942
user2030942

Reputation: 245

Fetch call in React creating some unexpected results when rendering

Problem summary

I'm attempting to display a set of three newsfeeds. Each newsfeed draws from a specific RSS feed.

My React Newsfeeds component receives a list of newsfeeds as props. Each item in that list has the RSS URL that I need to fetch from for each specific feed. However, for the purposes of this question, I'm not really using that data. In this case, for testing, I'm just using the same URL for all three newsfeeds.

I'm successfully retrieving each specific newsfeed RSS using a fetch call in componentDidMount(). Each feed is getting built by the mapFeed() function which is making the fetch call and constructing the HTML. These feeds are getting stored in the feeds state variable.

Finally, my component is supposed to be rendering this feeds state variable.

Something is getting lost in translation or there's something obvious that I'm missing, but by the time we get to the last render() I'm not able to access the information inside each feed.

What I've tried

Here is what I've tried, and my leads:

  1. I've tried logging any state changes into my console. I see that my this.state.stories array doesn't get fetched right away. I believe this is due to fetch working asynchronously. Once it gets fetched completely, I see that this.state.stories gets updated but no further rendering occurs.

  2. All my function logs (i.e entering function A...) are getting logged before the fetch call finishes getting the data. I'm unsure if this is due to the nature of the console and/or whether things are getting logged in time. But this feels like a clue

  3. I've tried updating this.state.feeds once more after the fetch call finishes. But this doesn't trigger a re-render either.

  4. I've dug through all kinds of different fetch code examples here to see if there's anything wrong with my fetch call, but none of them seem to fix the issue.

I'm sorry if this is kind of messy. I'm still at a basic-intermediate level with React, and I'm trying to grasp the concept of fetch.

Thanks for your support

My Code

class Newsfeeds extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      rssUrl: '', // current RSS URL
      stories: null, // stories for the current feed getting mapped
      feeds: null, // newsfeeds
    };
  }

  componentDidMount() {
    this.getFeeds()
  }

  getFeeds() {
    const feeds = this.props.data.list.map((item, index) => this.mapFeed(item, index));

    this.setState({
      feeds: feeds
    })
  }


  mapFeed(item, index) {
    const RSS_URL = 'https://www.democracynow.org/democracynow.rss'
    this.setState({ rssUrl: RSS_URL });
    this.getData(RSS_URL);


    return (
      <div className="newsfeed" key={index}>
        <div className="row align-items-center">
          <div className="col-md-9 newsFeedsItemStories">
            {this.getStories()}
          </div>
        </div>
      </div>
    )
  }

  getData = async (rssUrl) => {
    await fetch(rssUrl)
      .then((response) => {
        if (!response.ok) {
          console.log(`Did not get an ok from partner news source. got: ${response.statusText}`);
        }
        return response.text()
      })
      .then(str => new window.DOMParser().parseFromString(str, "text/xml"))
      .then(data => this.setStories(data))
      .catch((error) => {
        console.log(`Error getting rss data: ${error.message}`)
      })
  }

  setStories(data) {
    this.setState({
      stories: data.querySelectorAll("item"),
    })
  }

  getStories() {
    return (
      <div>
        <h4>{this.getTitle(this.state.stories[0])}</h4>
        <p>{this.getDate(this.state.stories[0])}</p>
        <h4>{this.getTitle(this.state.stories[1])}</h4>
        <p>{this.getDate(this.state.stories[1])}</p>
        <h4>{this.getTitle(this.state.stories[2])}</h4>
        <p>{this.getDate(this.state.stories[2])}</p>
      </div>
    )
  }

  /** Parse news item helper functions */
  getDateStr(story) {
    /** ...... some code to get date */
  }

  getTitle(story) {
    /** ...... some code to get title */
  }

  getUrl(story) {
    /** ...... some code to get url */
  }
  /** End of parse News item helper functions */

  render() {
    return (
      <section className="newsfeeds" id="newsfeeds">
        <h3>Title for my set of newsfeeds</h3>
        <div className="newsfeeds-list">
          {this.state.feeds}
        </div>
      </section>
    )
  }
}

export default Newsfeeds;

My console log

mapping feed
this.state.stories: 
null
building html for stories..
this.state.stories: 
null
mapping feed
this.state.stories: 
null
building html for stories..
this.state.stories: 
null
mapping feed
this.state.stories: 
null
building html for stories..
this.state.stories: 
null
component will unmount
mapping feed
this.state.stories: 
null
building html for stories..
this.state.stories: 
null
mapping feed
this.state.stories: 
null
building html for stories..
this.state.stories: 
null
mapping feed
this.state.stories: 
null
building html for stories..
this.state.stories: 
null
componentDidUpdate
State 'rssUrl' changed
State 'feeds' changed
componentDidUpdate
State 'feeds' changed
updating stories state
componentDidUpdate
State 'stories' changed
updating stories state
componentDidUpdate
State 'stories' changed
updating stories state
componentDidUpdate
State 'stories' changed
updating stories state
componentDidUpdate
State 'stories' changed
updating stories state
componentDidUpdate
State 'stories' changed
updating stories state
componentDidUpdate
State 'stories' changed

Upvotes: 0

Views: 53

Answers (2)

user2030942
user2030942

Reputation: 245

Following @user2030942 's advice, I've simplified my code heavily and it now works perfectly. Here is my solution if anybody else has this problem. I added a new variable isFetching, to not render anything unless fetching is done. I also added a couple ? in my map functions to check if data is ready before proceeding.

class Newsfeeds extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      active: -1,
      rssUrl: '',
      feeds: [],
    };
  }

  componentDidMount() {
    this.getFeeds()
  }

  /** setActive() sets the active feed for the toggle */

  setActive(index) {
    if (this.state.active != index) {
      this.setState(
        { active: index }
      )
    } else {
      this.setState(
        { active: -1 }
      )
    }
  }

  /** Parse news item helper functions */

  getDateStr(story) {
    const month = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];

    const date = new Date(story.querySelector("pubDate").innerHTML);

    var strDate = 'm d, Y'
      .replace('Y', date.getFullYear())
      .replace('m', month[date.getMonth()])
      .replace('d', date.getDate());

    return strDate;
  }

  getTitle(story) {
    let title = story.querySelector("title").innerHTML;

    return title;
  }

  getUrl(story) {
    return story.querySelector("link").innerHTML;
  }

  /** getFeeds() retrieves the newsfeeds from the RSS url's provided */

  getFeeds() {
    let feeds = []
    let fetchCount = 0
    let numFeeds = this.props.data.list.length

    this.props.data.list.map((item, index) => {
      const RSS_URL = item.rss_feed_url

      fetch(RSS_URL)
        .then((response) => {
          if (!response.ok) {
            console.log(`Did not get an ok from partner news source. got: ${response.statusText}`)
          }
          return response.text()
        })
        .then(str => new window.DOMParser().parseFromString(str, "text/xml"))
        .then(data => {
          const stories = data.querySelectorAll("item:nth-of-type(-n+3)");

          feeds.push(stories)

          this.setState({
            feeds: feeds
          })

          fetchCount++

          if (fetchCount == numFeeds) {
            this.setState({
              isFetching: false
            })
          }
        })
        .catch((error) => {
          console.log(`Error getting rss data: ${error.message}`)
        })
    })
  }

  render() {
    if (this.state.isFetching) {
      return false
    } else {
      return (
        <section className={styles.newsfeeds} id="newsfeeds">
          <div className="container">
            <h3>{this.props.data.title}</h3>
            <div className={styles.inner}>
              <div className={styles.newsfeedsList}>
                {
                  this?.state?.feeds?.map((feed, index) => {
                    let numStories = this.state.feeds.length

                    const stories = [...feed]?.map((item, index) => {
                      const title = this.getTitle(item)
                      const date = this.getDateStr(item)
                      const url = this.getUrl(item)

                      if ((index == 0)) {
                        return (
                          <div className={styles.newsFeedsItemStory}>
                            <a href={url}><h4>{title}</h4></a>
                            <p>{date}</p>
                          </div>
                        )
                      } else if (numStories >= 3) {
                        return (
                          <div className={styles['toggle']}>
                            <div className={styles.newsFeedsItemStory}>
                              <a href={url}><h4>{title}</h4></a>
                              <p>{date}</p>
                            </div>
                          </div>
                        )
                      } else {
                        return (
                          <div className="notice">
                            <h4>No stories available for this newsfeed</h4>
                          </div>
                        )
                      }
                    }) // end of stories map

                    return (
                      <div className={`${styles.newsfeedsItem} ${this.state.active === index ? styles['is-active'] : styles['is-hidden']}`} key={index}>
                        <div className="row align-items-center">
                          <div className="col-md-2 newsFeedsItemImg">
                            {
                              this.props.data.list[index] && (
                                <img src={this.props.data.list[index].image.sourceUrl} alt="" />
                              )
                            }
                          </div>
                          <div className="col-md-9 newsFeedsItemStories">
                            {stories}
                          </div>
                          <div className="col-md-1 text-right newsFeedsItemToggle">
                            <div className={styles['expand-feed']} onClick={() => this.setActive(index)}>
                              <button>
                                <span></span>
                                <span></span>
                              </button>
                            </div>
                          </div>
                        </div>
                      </div>
                    )
                  }) // end of data list map
                }
              </div>
            </div >
          </div >
        </section >
      ) // end of render return
    }

  } // end of render
}

export default Newsfeeds;

Upvotes: 0

CalebJamesStevens
CalebJamesStevens

Reputation: 11

Something to note is that you are using async/await with .then which isn't necessary.

Also in this line

const feeds = this.props.data.list.map((item, index) => this.mapFeed(item, index));

this.props.data is undefined if it's not being set. Where is this being passed in?

Either way I'm pretty sure your mistake is here

    this.getData(RSS_URL);

this code is most likely not finishing before you call getStories() a couple lines down. So your call to getStories is mapping nothing

Edit:

The main issue is you trying to update things before theyre ready. Trying to use async data before its loaded and state before it's done being set. Try consolidating

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      rssUrl: '', // current RSS URL
      stories: null, // stories for the current feed getting mapped
      feeds: [], // newsfeeds
    };
  }

  componentDidMount() {
    this.getFeeds()
  }

  getFeeds() {
    ['item'].map((item, index) => {
      const RSS_URL = 'https://www.democracynow.org/democracynow.rss'
      fetch(RSS_URL)
        .then((response) => {
          if (!response.ok) {
            console.log(`Did not get an ok from partner news source. got: ${response.statusText}`);
          }
          return response.text()
        })
        .then(str => new window.DOMParser().parseFromString(str, "text/xml"))
        .then(data => {
          const stories = data.querySelectorAll("item");

          this.setState((state) => {
            return {feeds: [
              ...state.feeds, 
              stories
            ]}
          })
        })
        .catch((error) => {
          console.log(`Error getting rss data: ${error.message}`)
        })
    })
  }

  render() {
    return (
      <section className="newsfeeds" id="newsfeeds">
        <h3>Title for my set of newsfeeds</h3>
        <div className="newsfeeds-list">
          {this?.state?.feeds?.map((feed, index) => {
            const stories = [...feed]?.map((item, index) => {
              const title = item.querySelector('title').innerHTML
              return (<div className="newsfeed" key={index}>
                <div className="row align-items-center">
                  <div className="col-md-9 newsFeedsItemStories">
                    <h4>{title}</h4>
                  </div>
                </div>
              </div>)
            })

            return stories
          })}
        </div>
      </section>
    )
  }
}

Upvotes: 1

Related Questions