【问题标题】:this.setState inside Promise cause strange behaviorPromise 中的 this.setState 导致奇怪的行为
【发布时间】:2017-12-31 21:58:35
【问题描述】:

简化问题。在 Promise 中调用 this.setState,在未决 Promise 结束之前渲染。

我的问题是:

  1. this.setState 不会立即返回
    • 我希望它是异步的,因此待处理的 Promise 将首先关闭。
  2. 如果渲染函数内部出现问题,则调用 Promise 中的 catch。
    • 可能与 1) 相同的问题是,渲染似乎仍在调用 this.setState 的承诺的上下文中。

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

当前日志显示:

  • 渲染数据,[]
  • 开始 makeRankRequestCall-rankrequests
  • 渲染数据,[...]
  • _makeRankRequestCall-promisecatch 错误:未知的命名模块...
  • 渲染数据,[...]
  • 可能未处理的承诺

安卓模拟器 “反应”:“16.0.0-alpha.12”, "react-native": "0.46.4",

编辑: 围绕 this.setState 包裹 setTimeout 也可以

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

EDIT2: 在 react-native github 中并行创建了一个错误报告 https://github.com/facebook/react-native/issues/15214

【问题讨论】:

  • 我无法准确确定您要解决的问题。您是否尝试在执行 console.log("done"); 时只重新渲染一次?如果是这样,实现它的一种方法是覆盖shouldComponentUpdate(),因此当您准备好重新渲染时,它总是返回falsethis.forceUpdatefacebook.github.io/react/docs/react-component.html#forceupdate

标签: react-native


【解决方案1】:

Promisethis.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
})

每次执行 this.setState() 时都会重新渲染,因此您会在整个承诺得到解决之前更新您的组件。这个问题可以通过将 componentDidMount() 设置为异步函数并使用 await 来解决承诺来解决:

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

希望对你有帮助。

【讨论】:

  • 我不建议更改给定组件的 RN-Lifecycle 方法这样重要的东西。使其异步可能最终成为一个大错误。
  • 另外,如果你正在做任何事情来响应状态更新,你应该使用 RN-Lifecycle 方法componentDidUpdate
【解决方案2】:

我也很难理解你在这里试图做什么,所以我尝试了一下。

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

}

【讨论】:

    【解决方案3】:

    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 块都会捕获代码中任何地方抛出的任何错误。根据specs

    throw 语句引发用户定义的异常。当前函数的执行将停止(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>
      );
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-11-27
      • 1970-01-01
      • 2016-05-13
      • 2017-07-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多