Simplified issue. Calling this.setState inside a Promise, renders before ends pending Promise.
My problems are:
import dummydata_rankrequests from "../dummydata/rankrequests";
class RankRequestList extends Component {
constructor(props) {
super(props);
this.state = { loading: false, data: [], error: null };
this.makeRankRequestCall = this.makeRankRequestCall.bind(this);
this.renderItem = this.renderItem.bind(this);
}
componentDidMount() {
// WORKS AS EXPECTED
// console.log('START set');
// this.setState({ data: dummydata_rankrequests.data, loading: false });
// console.log('END set');
this.makeRankRequestCall()
.then(done => {
// NEVER HERE
console.log("done");
});
}
makeRankRequestCall() {
console.log('call makeRankRequestCall');
try {
return new Promise((resolve, reject) => {
resolve(dummydata_rankrequests);
})
.then(rankrequests => {
console.log('START makeRankRequestCall-rankrequests', rankrequests);
this.setState({ data: rankrequests.data, loading: false });
console.log('END _makeRankRequestCall-rankrequests');
return null;
})
.catch(error => {
console.log('_makeRankRequestCall-promisecatch', error);
this.setState({ error: RRError.getRRError(error), loading: false });
});
} catch (error) {
console.log('_makeRankRequestCall-catch', error);
this.setState({ error: RRError.getRRError(error), loading: false });
}
}
renderItem(data) {
const height = 200;
// Force a Unknown named module error here
return (
<View style={[styles.item, {height: height}]}>
</View>
);
}
render() {
let data = [];
if (this.state.data && this.state.data.length > 0) {
data = this.state.data.map(rr => {
return Object.assign({}, rr);
});
}
console.log('render-data', data);
return (
<View style={styles.container}>
<FlatList style={styles.listContainer1}
data={data}
renderItem={this.renderItem}
/>
</View>
);
}
}
Currrent logs shows:
Android Emulator "react": "16.0.0-alpha.12", "react-native": "0.46.4",
EDIT: wrapping setTimeout around this.setState also works
setTimeout(() => {
this.setState({ data: respData.data, loading: false });
}, 1000);
EDIT2: created a bug report in react-native github in parallel https://github.com/facebook/react-native/issues/15214
Both Promise
and this.setState()
are asynchronous in javascript. Say, if you have the following code:
console.log(a);
networkRequest().then(result => console.log(result)); // networkRequest() is a promise
console.log(b);
The a and b will get printed first followed by the result of the network request.
Similarly, this.setState()
is also asynchronous so, if you want to execute something after this.setState()
is completed, you need to do it as:
this.setState({data: rankrequests.data}, () => {
// Your code that needs to run after changing state
})
React Re-renders every time this.setState()
gets executed, hence you are getting your component updated before the whole promise gets resolved. This problem can be solved by making your componentDidMount()
as async function and using await to resolve the promise:
async componentDidMount() {
let rankrequests;
try {
rankrequests = await this.makeRankRequestCall() // result contains your data
} catch(error) {
console.error(error);
}
this.setState({ data: rankrequests.data, loading: false }, () => {
// anything you need to run after setting state
});
}
Hope it helps.
I too am having a hard time understanding what you are attempting to do here so I took a stab at it.
Since the this.setState()
method is intended to trigger a render, I would not ever call it until you are ready to render. You seem to relying heavily on the state variable being up to date and able to be used/manipulated at will. The expected behaviour here, of a this.state.
variable, is to be ready at the time of render. I think you need to use another more mutable variable that isn't tied to states and renders. When you are finished, and only then, should you be rendering.
Here is your code re-worked to show this would look:
import dummydata_rankrequests from "../dummydata/rankrequests";
class RankRequestList extends Component {
constructor(props) {
super(props);
/*
Maybe here is a good place to model incoming data the first time?
Then you can use that data format throughout and remove the heavier modelling
in the render function below
if (this.state.data && this.state.data.length > 0) {
data = this.state.data.map(rr => {
return Object.assign({}, rr);
});
}
*/
this.state = {
error: null,
loading: false,
data: (dummydata_rankrequests || []),
};
//binding to 'this' context here is unnecessary
//this.makeRankRequestCall = this.makeRankRequestCall.bind(this);
//this.renderItem = this.renderItem.bind(this);
}
componentDidMount() {
// this.setState({ data: dummydata_rankrequests.data, loading: false });
//Context of 'this' is already present in this lifecycle component
this.makeRankRequestCall(this.state.data).then(returnedData => {
//This would have no reason to be HERE before, you were not returning anything to get here
//Also,
//should try not to use double quotes "" in Javascript
//Now it doesn't matter WHEN we call the render because all functionality had been returned and waited for
this.setState({ data: returnedData, loading: false });
}).catch(error => {
console.log('_makeRankRequestCall-promisecatch', error);
this.setState({ error: RRError.getRRError(error), loading: false });
});
}
//I am unsure why you need a bigger call here because the import statement reads a JSON obj in without ASync wait time
//...but just incase you need it...
async makeRankRequestCall(currentData) {
try {
return new Promise((resolve, reject) => {
resolve(dummydata_rankrequests);
}).then(rankrequests => {
return Promise.resolve(rankrequests);
}).catch(error => {
return Promise.reject(error);
});
} catch (error) {
return Promise.reject(error);
}
}
renderItem(data) {
const height = 200;
//This is usually where you would want to use your data set
return (
<View style={[styles.item, {height: height}]} />
);
/*
//Like this
return {
<View style={[styles.item, {height: height}]}>
{ data.item.somedataTitleOrSomething }
</View>
};
*/
}
render() {
let data = [];
//This modelling of data on every render will cause a huge amount of heaviness and is not scalable
//Ideally things are already modelled here and you are just using this.state.data
if (this.state.data && this.state.data.length > 0) {
data = this.state.data.map(rr => {
return Object.assign({}, rr);
});
}
console.log('render-data', data);
return (
<View style={styles.container}>
<FlatList
data={data}
style={styles.listContainer1}
renderItem={this.renderItem.bind(this)} />
{ /* Much more appropriate place to bind 'this' context than above */ }
</View>
);
}
}
The setState
is indeed asynchronous. I guess makeRankRequestCall
should be like this:
async makeRankRequestCall() {
console.log('call makeRankRequestCall');
try {
const rankrequests = await new Promise((resolve, reject) => {
resolve(dummydata_rankrequests);
});
console.log('START makeRankRequestCall-rankrequests', rankrequests);
this.setState({ data: rankrequests.data, loading: false });
console.log('END _makeRankRequestCall-rankrequests');
} catch(error) {
console.log('_makeRankRequestCall-catch', error);
this.setState({ error: RRError.getRRError(error), loading: false });
}
}
Secondly, promise catching an error of renderItem
is perfectly fine. In JavaScript, any catch block will catch any error that is being thrown anywhere in the code. According to specs:
The throw statement throws a user-defined exception. Execution of the current function will stop (the statements after throw won't be executed), and control will be passed to the first catch block in the call stack. If no catch block exists among caller functions, the program will terminate.
So in order to fix it, if you expect renderItem
to fail, you could do the following:
renderItem(data) {
const height = 200;
let item = 'some_default_item';
try {
// Force a Unknown named module error here
item = styles.item
} catch(err) {
console.log(err);
}
return (
<View style={[item, {height: height}]}>
</View>
);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With