jchi2241
jchi2241

Reputation: 2226

Improperly updating state with async / await in React

My goal is to add a new stock - which consists of a quote, chart, meta info into their respective arrays in the component state, by calling my _addStock() function in componentDidMount. However, after looping through and calling _addStock, I only have 1 stock within each respective array.

I can get this working by adding a delay via setTimeouts between each _addStock call - but that defeats the purpose of promises.

I thought with async / awaits, JavaScript would "synchronously" execute code. However, it seems like the setState is only being called once when I'm calling _addStock multiple times.

What's going on?

_fetchMeta, _fetchQuote, ...etc return promises.

...

// this is what my state ends up looking like, only 1 per key when I'm expecting 4.
this.state = {
    stocks: [
        {
            symbol: 'MSFT',
            display: '...',
            color: '...'
        }
    ],
    metas: [
        {
            symbol: 'MSFT',
            name: '...',
            description: '...'
        }
    ],
    quotes: [
        {
            symbol: 'MSFT',
            price: '...',
            change: '...'
        }
    ],
    charts: [
        {
            symbol: 'MSFT',
            data: "..."
        }
    ]
}

//...

_addStock = async symbol => {
    const { metas, quotes, charts, stocks } = this.state;

    // check if stock already exists
    const hasStock = stocks.filter(s => s.symbol === symbol).length;
    if (hasStock) {
        return;
    }

    const meta = await this._fetchMeta(symbol);
    const quote = await this._fetchQuote(symbol);
    const chart = await this._fetchChart(symbol);
    const stock = {
        symbol: symbol.toUpperCase(),
        color: setStockColor(),
        display: true
    };

    this.setState({
        ...this.state,
        metas: [...metas, meta],
        quotes: [...quotes, quote],
        charts: [...charts, chart],
        stocks: [...stocks, stock]
    });
};

//...

componentDidMount() {
    const initStocks = ["FB", "MSFT", "NVDA", "AAPL"];
    initStocks.forEach(s => this._addStock(s));
}

Upvotes: 0

Views: 2263

Answers (3)

Rehan Haider
Rehan Haider

Reputation: 1061

When you call setState multiple time in short interval react batch all setState calls for performance and execute them collectively, that's why when to call setState 2nd time first request is still pending and component state is empty at the end only one item is inserted.

You can use following approach to bring data asynchronously, and it will update state only once with final data. As it is not using state for each iteration that's why it will maintain data consistency too.

_addStock = symbols => {
  const { metas, quotes, charts, stocks } = this.state;
  // a variable to keep track how many symbols have been processed
  let count = 0;

  symbols.forEach(async symbol => {
    // check if stock already exists
    const hasStock = stocks.some(s => s.symbol === symbol);
    if (!hasStock) {
      //make all api requests in parallel
      let meta = this._fetchMeta(symbol);
      let quote = this._fetchQuote(symbol);
      let chart = this._fetchChart(symbol);
      let stock = {
        symbol: symbol.toUpperCase(),
        color: setStockColor(),
        display: true
      };

      //wait for responses
      meta = await meta;
      quote = await quote;
      chart = await chart;

      //add all values to old state
      metas.push(meta);
      quotes.push(quote);
      chart.push(chart);
      stock.push(stock);
    }

    //update count
    ++count;

    //if all values have been retrieved update state
    if(count === symbols.length){
      this.setState({
        ...this.state,
        metas: [...metas],
        quotes: [...quotes],
        charts: [...charts],
        stocks: [...stocks]
      });
    }
  });
};

componentDidMount() {
    const initStocks = ["FB", "MSFT", "NVDA", "AAPL"];
    //Pass whole array to _addStock
    this._addStock(initStocks);
}

Upvotes: 0

An Nguyen
An Nguyen

Reputation: 1478

For fetching multiple URLs in parallel using Promise.all. A roughly implement in your case would be:

_addStock = async symbol => {
    ...
    let fetchMeta = this._fetchMeta.resolve(symbol);
    let fetchQuote = this._fetchQuote.resolve(symbol);
    let fetchMeta = this._fetchChart.resolve(symbol);

    let data = await Promise.all([
      fetchMeta,
      fetchQuote,
      fetchChart,
    ]).then((data) => {
      console.log(data); 
      // Inspect your data here setState accordingly        
      this.setState({ ... });    
    });        
};

Upvotes: 1

RIYAJ KHAN
RIYAJ KHAN

Reputation: 15292

calling _addStock multiple time,call setState multiple time.Its cause multiple setState operation to run in parallel without waiting confirmation of previous setState update. As a setState async operation, you get such weird behaviour.

To fix it,you can add one more level of async/await with promise

_addStock = async symbol => {

    return new Promise((resolve,reject)){

        this.setState({
            ...this.state,
            metas: [...metas, meta],
            quotes: [...quotes, quote],
            charts: [...charts, chart],
            stocks: [...stocks, stock]
        },()=>{

            resolve(true); //give confirmation setState is updates successfully.
        });
    }
};

call _addStock with await.

componentDidMount() {
    const initStocks = ["FB", "MSFT", "NVDA", "AAPL"];
    initStocks.forEach( async s => {
            await this._addStock(s)
    });
}

Upvotes: 0

Related Questions