This.setState inside Promise causes strange behavior

Simplified problem. Calling this.setState inside a Promise displays the pending Promise to the end.

My problems:

  • This .setState does not return immediately
    • I expected it to be asynchronous, so the pending promise would be closed first.
  • If something breaks inside the render function, catch is called inside Promise.
    • Perhaps the same problem as 1) that it seems that the render is still in the context of the promise in which 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 magazines show:

  • rendering data, []
  • START makeRankRequestCall-rankrequests
  • rendering data, [...]
  • _makeRankRequestCall-promisecatch Error: unknown named module ...
  • rendering data, [...]
  • Possible raw promises

Android Emulator "": "16.0.0-alpha.12", "-": "0.46.4",

: wrapping setTimeout this.setState

    setTimeout(() => {
      this.setState({ data: respData.data, loading: false });
    }, 1000);

EDIT2: github https://github.com/facebook/react-native/issues/15214

+6
3

Promise this.setState() javascript. , :

console.log(a);
networkRequest().then(result => console.log(result)); // networkRequest() is a promise
console.log(b);

a b, .

, this.setState() , , - this.setState(), :

this.setState({data: rankrequests.data}, () => {
  // Your code that needs to run after changing state
})

Re- , this.setState(), , . , componentDidMount() async :

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

, .

+1

, , .

this.setState() , , . , , , / . this.state. . , , . , , .

, , :

dummydata_rankrequests "../dummydata/rankrequests";

RankRequestList {

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

}

+1

setState . , makeRankRequestCall :

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

-, renderItem . JavaScript catch , . :

throw . ( , ), catch . catch, .

, , , renderItem , :

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