Double rendering in React with asynchronous call in componentDidMount causing error - javascript

I'm building a blog application that has an articles index page, and from there you can click on an article to see the article or edit the article.
If you're going from the index page to the edit page, it works just fine because I already have all the articles in state. But if I refresh after I've gone to the edit-article page, I no longer have all the articles in state.
This is a problem because I'm making an asynchronous recieveSingleArticle call in the componentDidMount of my edit-article page, then I setState so my form is prepopulated. There's a double render which causes an "Uncaught TypeError: Cannot read property 'title' of undefined" error, presumably during the first render before the article has been received into state.
class ArticleEdit extends React.Component {
constructor(props) {
super(props);
this.state = {title: "", body: "", imageFile: ""};
this.handleChange = this.handleChange.bind(this);
this.handlePublish = this.handlePublish.bind(this);
this.handleFile = this.handleFile.bind(this);
this.handleCancel = this.handleCancel.bind(this);
}
componentDidMount() {
const { article, requestSingleArticle } = this.props;
requestSingleArticle(this.props.match.params.articleID)
.then(() => {
this.setState({
title: article.title,
body: article.body,
imageFile: article.imageFile
});
});
}
...
I tried wrapping my async calls inside of an "if (this.props.article)" but that didn't work. Is there a best way of dealing with this type of problem? Any advice greatly appreciated!
UPDATE:
Another solution that works is to have a componentDidUpdate in addition to componentDidMount. check in componentDidMount if this.props.article exists and if so, setState. And in componentDidUpdate, wrap the setState in the following conditional:
if (!prevProps.article && this.props.article)

Just check if the article is present in the props before calling async action
componentDidMount() {
const { article, requestSingleArticle } = this.props;
if (!(article && requestSingleArticle)) return; // this line
requestSingleArticle(this.props.match.params.articleID)
.then(() => {
this.setState({
title: article.title,
body: article.body,
imageFile: article.imageFile
});
});
}
Since you are not getting any render from this method , it means that the props are not yet obtained in the life cycle method componnetDidMount. So instead you can use componentWillReceiveProps like this
componentWillReceiveProps(nextProp) {
// this line here will check the article props' status so that
// we will not use setState each time we get a prop
if (this.props.article === nextProp.article) return;
// rest is just the same code from above
const { article, requestSingleArticle } = nextProp;
if (!(article && requestSingleArticle)) return; // this line
requestSingleArticle(this.props.match.params.articleID)
.then(() => {
this.setState({
title: article.title,
body: article.body,
imageFile: article.imageFile
});
});
}

Related

React Class passing stale props to Child stateless component

My Parent Component represents a form.
The users filling in the form have access to information in the form that is updated in real time as they update certain fields.
The Issue I am running into is. On one of these updates when we fetch the new data and pass it to the child randomly sometimes the child is receiving stale props. From the previous request.
The structure is something like this.
export class Form extends React.Component<Props, State> {
fetchUpdates = async (payload) => {
this.setState({ isLoadingUpdates: true })
await Service.getUpdates(payload)
.then(response => {
this.setState({ isLoadingUpdates: false, updates: response.data })
})
.catch(({ data: errors }) => this.setState({ isLoadingUpdates: false }))
}
}
render () {
const {
updates,
isLoadingUpdates,
} = this.state
<FormCombobox
onChange={this.fetchUpdates}
md={10}
name="field"
id="field"
label="Field"
onMenuOpen={() => forceCheck()}
openMenuOnClick
selectRef={this.itemSelect}
value={values.item}
options={itemOptions || []}
/>
<Info
data={updates}
errorMessage={this.state.updatesError}
/>
}
}
It doesn't occur every time but randomly either when the form is first updated or on one of the following updates the < Info > container recieves the previous requests response data. How can I stop the parent from passing stale data?
The problem here is that when fetchUpdates is called multiple times it gets out of order due to network delay. Let's say fetchUpdates is called three times, and let's say the request takes 5, 2 and 4 seconds respectively to complete. In this case, you can see that the second request calls setState before the first request. As a result, the info component gets passed the first value after the second value. This is the reason why it is intermittent.
Using await here won't help, because the fetchUpdates function calls are independent of each other.
One more thing, I noticed that you have isLoadingUpdates. But it's not being used anywhere in the code. And also doing,
if (!this.state. isLoadingUpdates) {
await Service.getUpdates(payload)
.then(response => {
this.setState({ isLoadingUpdates: false, updates: response.data })
})
.catch(({ data: errors }) => this.setState({ isLoadingUpdates: false }))
}
won't work because then it means you will miss keypresses when the network call is ongoing.
I would suggest using a debounce for the inputs. You can find how to do debounce here: Perform debounce in React.js

Using Map in React with firebase

Someone please help. I have combed all of the doc's, examined all similar questions and really cannot see what I am missing.
I am new to react and trying to map over documents returned from an asynchronous call from a firebase db.
Here is the code:
class Posts extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
};
}
componentDidMount() {
let posts = [];
db.collection("Posts").get().then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
posts.push({
data: doc.data(),
id: doc.id
});
});
});
this.setState({
data: posts
});
}
renderPosts() {
console.log(this.state.data);
return this.state.data.map((post, index) => {
console.log(post);
console.log(index);
return (
<div>
{index}
</div>
)
})
}
render() {
return(
<div>
{this.renderPosts()}
</div>
);
}
}
I am sure it is something super simple but I am pulling my hair out. If I examine my state I can see the data. console.log(this.state.data); inside renderPosts even displays exactly what I need to map over, but everything inside the map callback doesn't return and I end up with a single empty Any help would be greatly appreciated.
As Li357 commented, the setState call needs to happen inside the get() callback:
db.collection("Posts").get().then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
posts.push({
data: doc.data(),
id: doc.id
});
});
this.setState({
data: posts
});
});
The reason for this is that data is loaded from Firestore asynchronously, and by the time you were calling setState the posts variable was still empty.
The easiest way to understand this type of asynchronous loading is with a few log lines:
console.log("Before starting to get documents");
db.collection("Posts").get().then(function(querySnapshot) {
console.log("Got documents");
});
console.log("After starting to get documents");
When you run this, the output is:
Before starting to get documents
After starting to get documents
Got documents
That is probably not the order you expected the output in. But it is the expected behavior: instead of blocking the code while the data is loaded (which would freeze the browser), the code after the get().then() block immediately executes. And then when the data is loaded, the code in the then callback is executed. That's why all code that needs the documents from the database needs to be inside the then callback.

prevState not consistent

I am trying to add thumbnail URL into my thumbnail state array. But also to prevent an infinite loop I am trying to compare the previous state from the current state. However, when doing so the previous state keeps referring back to the state object? The array that I need is the one that shows Explore line 35. And ideally I thought that would be the current State, since that is what it is printing.
Constructor
constructor(props) {
super(props);
this.props.getVideos();
this.state = {
thumbnail: []
};
}
ComponentDidUpdate
componentDidUpdate(prevState) {
console.log("Current State->" + this.state.thumbnail);
console.log("Previous State->" + JSON.stringify(prevState.videos));
// console.log("Previous State->" + prevState.videos.video);
if (this.props.videos == null) {
console.log("It's null");
} else {
if (this.state !== prevState) {
const videos = this.props.videos.video.map(video => {
this.setState(prevState => ({
thumbnail: prevState.thumbnail.concat(video.thumbnail)
}));
console.log(this.state.thumbnail);
});
}
}
}
Reducers snippet
case GET_VIDEOS:
return {
...state,
videos: action.payload,
loading: false
};
Few issues that I can see right away:
if (this.state !== prevState) is not a valid way of comparing the old state to the new. They are just comparing their references as variables (which will always be different) - not their contents. To do this properly you need to compare each state variable one-by-one. Either use a library for that, such as lodash's _isEqual or implement your own solution (good idea if your state is small).
Doing console.log(this.state.thumbnail); right after your setState() will actually print the previous value because setState() is asynchronous. Instead, put your log inside the callback parameter of setState():
this.setState(prevState => ({
thumbnail: prevState.thumbnail.concat(video.thumbnail)
}), () => console.log(this.state.thumbnail));

Consuming REST resources before rendering in React.js

I have React components that consumes external webservice to reach data for rendering.
I would like the data being loaded BEFORE the rendering because I want it to be indexed by search engines.
Here is my component :
class AboutPage extends React.Component {
async componentWillMount() {
let response = await EventWS.doSearch();
this.setState({
eventList : response.data
})
}
render() {
/* ... */
}
}
(I tried to use async/await because I thought it could help, but no).
When trying to load the page with server side rendering I got the warning :
Warning: setState(...): Can only update a mounting component. This usually means you called setState() outside componentWillMount() on the server. This is a no-op. Please check the code for the FluxContainer(AboutPage) component.
Indicating that the setState is done AFTER the componentWillMount has ended.
In my specific case, what is best way to acheving this ? Is there a easy way to do an ajax call synchronously ? Is it recommended to do so ?
Thank you.
EDIT :
I have found a library that allow to make synchronous call :
https://github.com/ForbesLindesay/sync-request
But it states that it is not well suited for production. So I'am a bit disapointed.
I dont have a lot of experience with Flux but it seems like you should do this in the componentDidMount method.
You can use Axios promise based get in componentDidMount to implement it, for an example you can refer Handling Events In React and below sample code:
constructor(props) {
super(props);
this.state = {eventList: []};
this.Axios = axios.create();
}
componentDidMount() {
let _this = this;
this.Axios.get('/your/rest/url')
.then(function (response) {
console.log(response);
_this.setState({eventList: response.data});
}).catch(function (error) {
console.log(error);
});
}
Or if you are already using Axios then make sure EventWS.doSearch() return Promise to you and call it in componentDidMount, as follows:
constructor(props) {
super(props);
this.state = {eventList: []};
}
componentDidMount() {
let _this = this;
EventWS.doSearch().then(function (response) {
console.log(response);
_this.setState({eventList: response.data});
}).catch(function (error) {
console.log(error);
});
}

Redux async dispatch causes connected component error

When a button is clicked, then an API call is made (simulated by setTimeout) and when complete, that item is removed from the state. I would expect the component to not attempt to be rendered, but it is and fails, because the state it needs is no longer there.
The first fiddle showing the desired behaviour works because the call to dispatch is synchronous:
dispatch({
type: 'REMOVE',
id
});
The second fiddle shows the error because the call to dispatch is made asynchronously:
setTimeout(() => {
dispatch({
type: 'REMOVE',
id
});
}, 0);
I assumed the parent component would no longer try to render the child component because that item has been filtered out of the array. How do I remove the item from the state and prevent the component trying to re-render?
Yes this is an issue because by default redux doesn't support async calls like so what you are looking for it redux-thunk
https://github.com/gaearon/redux-thunk
Scroll down to motivation and it will basically have the exact same code you have posted here.
:::EDIT:::
Based off of your comment below I updated your code I think one of the main sources of the problem was connecting the Item component with map state to props.
const mapStateToProps = (state, { id }) => {
var {text} = state.items.filter((x) => x.id === id)[0];
return {
id: id,
text: text
};
};
const mapDispatchToProps = (dispatch) => {
return {
onRemove(id) {
setTimeout(() => {
dispatch({
type: 'REMOVE',
id
});
}, 0);
}
};
};
I know you probably didn't need it / or want it but I refactored the code a little bit to make it a little more clear but I might have gotten carried away and took it to far from your original example but if you want to take a look its posted here.
https://jsfiddle.net/kriscoulson/ogkyspsp/

Categories

Resources