0

I'm trying to use immutability-helper to update my React state asynchronously from multiple sources (API calls). However, it seems to me that this is not the way to go, since state always gets updated with values from a single source only. Can someone explain me why is this the case and how to properly handle updates of my state?

import React from 'react';
import update from 'immutability-helper';

class App extends React.Component {
  constructor(props, context) {
    super(props, context);
    this.state = {
      updateInterval: 60,  // in seconds
      apiEndpoints: [
        '/stats',
        '/common/stats',
        '/personal/stats',
        '/general_info',
      ],
      items: [
        { itemName: 'One', apiUrl: 'url1', stats: {} },
        { itemName: 'Two', apiUrl: 'url2', stats: {} },
      ],
    };
    this.fetchData = this.fetchData.bind(this);
  }

  componentDidMount() {
    this.fetchData();
    setInterval(() => this.fetchData(), this.state.updateInterval * 1000);
  }

  fetchData() {
    this.state.apiEndpoints.forEach(endpoint => {
      this.state.items.forEach((item, i) => {
        fetch(`${item.apiUrl}${endpoint}`)
          .then(r => r.json())
          .then(res => {
            // response from each endpoint contains different keys.
            // assign all keys to stats object and set default 0 to
            // those which don't exist in response
            const stats = {
              statsFromFirstEndpoint: res.first_endpoint ? res.first_endpoint : 0,
              statsFromSecondEndpoint: res.second_endpoint ? res.second_endpoint : 0,
              statsFromThirdEndpoint: res.third_endpoint ? res.third_endpoint : 0,
            };
            this.setState(update(this.state, {
              items: { [i]: { $merge: { stats } } }
            }));
          })
          .catch(e => { /* log error */ });
      });
    });
  }

  render() {
    return (
      <div className="App">
        Hiya!
      </div>
    );
  }
}

export default App;
errata
  • 5,695
  • 10
  • 54
  • 99
  • This question and accepted answer may provide some good info for you: https://stackoverflow.com/questions/37576685/using-async-await-with-a-foreach-loop – Anthony Apr 29 '18 at 19:16
  • Ah, so this is indeed a problem with my perceiving of asynchronicity? I'll check out that answer and see how could I apply it in this project... – errata Apr 29 '18 at 19:18
  • yeah I think `Promise.all()` might be your meal ticket – Anthony Apr 29 '18 at 19:22

1 Answers1

1

You should use the prevState argument in setState to make sure it always use the latest state:

this.setState(prevState =>
  update(prevState, {
    items: { [i]: { $merge: { stats } } },
  }));

Alternatively, map your requests to an array of promises then setState when all of them resolved:

const promises = this.state.apiEndpoints.map(endPoint =>
  Promise.all(this.state.items.map((item, i) =>
    fetch(), // add ur fetch code
  )));
Promise.all(promises).then(res => this.setState( /* update state */ ));
Roy Wang
  • 11,112
  • 2
  • 21
  • 42
  • This is sort of the way I went, but I am confused a bit how to exactly update my state now? In my example, I tried to update specific object in array (`items: { [i]: { $merge: { stats } } }`), but should I now define an object in outer scope of both loops and then add keys to it in my `fetch()` code and finally update it as in your example? – errata Apr 29 '18 at 20:08
  • That works but might be harder to read (side effect in map). For a pure functional approach, you can return `stats` in the fetch code, then construct an object from the resolved promise array result (using `reduce` or otherwise) and call `setState` on the object. – Roy Wang Apr 29 '18 at 20:19