Reputation: 245
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:
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.
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
I've tried updating this.state.feeds
once more after the fetch
call finishes. But this doesn't trigger a re-render either.
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
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
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