Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

this.setState inside Promise cause strange behavior

Tags:

react-native

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

like image 432
MortalFool Avatar asked Jul 25 '17 19:07

MortalFool


Video Answer


3 Answers

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.

like image 128
Dani Akash Avatar answered Oct 18 '22 02:10

Dani Akash


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

}

like image 1
GoreDefex Avatar answered Oct 18 '22 01:10

GoreDefex


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>
  );
}
like image 1
Andrey E Avatar answered Oct 18 '22 01:10

Andrey E