Sunday, August 6, 2017

this.setState inside Promise cause strange behavior

Leave a Comment

Simplified issue. Calling this.setState inside a Promise, renders before ends pending Promise.

My problems are:

  1. The this.setState is not immediatly returned
    • I expected it to be async, so that the pending promise will be closed first.
  2. If something will break inside the render function, the catch inside the Promise is called.
    • Maybe same issue as 1) that it seems like the render is still in context of the promise in which the this.setState was called.

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:

  • render-data, []
  • START makeRankRequestCall-rankrequests
  • render-data, [...]
  • _makeRankRequestCall-promisecatch Error: Unknown named module...
  • render-data, [...]
  • Possible Unhandled Promise

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

3 Answers

Answers 1

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.

Answers 2

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>     ); } 

}

Answers 3

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 Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment