0

I'm trying to request JSON data from two different URLs using fetch and format it with JSX before loading it into the state object. It's only retrieving the data from one of the URLs.

I have the URLs in an object like this:

var urls = {
  'recent': 'https://fcctop100.herokuapp.com/api/fccusers/top/recent',
  'alltime': 'https://fcctop100.herokuapp.com/api/fccusers/top/alltime',
};

I want to fetch the data by iterating over the object containing the URLs, turn it into an array of JSX objects, then store the result in my main object's state.

for (var key in urls) {
  var url = urls[key];
  fetch(url).then((response) => {
    response.json().then((json) => {
      var userNumber = 0;
      var html = json.map((user) => {
        return (
          <tr>
            <td>{++userNumber}</td>
            <td>
              <img src={user.img} alt={user.username} />
              {user.username}
            </td>
            <td>{user.recent}</td>
            <td>{user.alltime}</td>
          </tr>
        );
      }); // Close json.map block.
      this.setState({
        'data': { [key]: html }
      });
      console.log(key);
    }); // Close response.json block.
  }); // Close fetch block.
}

It should fetch the data from the recent URL and store the formatted result in this.state.data.recent. Then it should do the same thing for alltime, storing the result in this.state.data.alltime. What it actually seems to do is fetch alltime twice.

This makes no sense to me at all. I don't even have a guess as to what is happening.

I have noticed that if I put log statements into fetch's arrow function it shows that it's hitting both keys and both URLs, but if I put log statements at the end of the response.json() arrow function, after json.map, it shows alltime twice.

It's processing the second URL twice. I know that the order isn't guaranteed, but it does tend to process them in the order in which I defined them and I think that's significant here. Even if it is a problem with asynchronous requests happening at the same time, though, they're two different requests ultimately storing data in two different places, aren't they?

A version of this code is also at CodePen, but I'm likely to change that as I try to figure out what's going on here.

Full code:

var urls = {
  'recent': 'https://fcctop100.herokuapp.com/api/fccusers/top/recent',
  'alltime': 'https://fcctop100.herokuapp.com/api/fccusers/top/alltime',
};

function View(props) {
  return (
    <form className="view">
      <label>View: </label>
      <label><input type="radio" name="view" value="recent" checked={props.view == 'recent'} />Past 30 Days</label>
      <label><input type="radio" name="view" value="alltime" checked={props.view == 'alltime'} />All Time</label>
    </form>
  );
}

class Main extends React.Component {
  constructor() {
    super();
    this.state = {
      'view': 'recent',
      'data': {
        'recent': null,
        'alltime': null,
      },
    };
  }
  
  componentWillMount() {
    for (var key in urls) {
      var url = urls[key];
      console.log(key);
      console.log(url);
      fetch(url).then((response) => {
        response.json().then((json) => {
          var userNumber = 0;
          var html = json.map((user) => {
            return (
              <tr>
                <td>{++userNumber}</td>
                <td>
                  <img src={user.img} alt={user.username} />
                  {user.username}
                </td>
                <td>{user.recent}</td>
                <td>{user.alltime}</td>
              </tr>
            );
          }); // Close json.map block.
          
          console.log('Setting state for ' + key);
          this.setState({
            'data': { [key]: html }
          });
        }); // Close response.json block.
      }); // Close fetch block.
    }
    /*
    fetch(urls.recent).then((response) => {
      response.json().then((json) => {
        var view = 'recent';
        var html = json.map((user) => {
          return (
            <tr>
              <td>
                <img src={user.img} alt={user.username} />
                {user.username}
              </td>
              <td>{user.recent}</td>
              <td>{user.alltime}</td>
            </tr>
          );
        }); // Close json.map block.
        this.setState({
          'data': { [view]: html },
        });
      }); // Close response.json block.
    }); // Close fetch block.
    */
  }
  
  render() {
    return (
      <div>
        <View view={this.state.view} />
        <table>
          {this.state.data[this.state.view]}
        </table>
      </div>
    );
  }
}

ReactDOM.render(<Main />, document.getElementById('app'));
img {
  width: 2em;
  height: 2em;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<main id="app"></main>
halfer
  • 19,824
  • 17
  • 99
  • 186
Vince
  • 3,962
  • 3
  • 33
  • 58

1 Answers1

1

Async functions and for loops are one of the big topics in js (may search them on SO) . They can be solved using an IIFE or let:

for(let key in keys){
  //your code, key will be locally scoped
}

or:

for(key in keys){
(function(key){
   //key is locally scoped
 })(key);
 }

What happens? Youve got just one key, so it will just have one value at a time, the for loop will finish before the async functions execute:

key="recent";
key="alltime";
//async functions run

Why does local ( block) scoping helps? Well:

 {
 key="recent";
 //async function one fires, key is recent
 }
 {
 key="alltime";
 //async function two fires, key is alltime
 }
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • I had a feeling this had something to do with the asynchronous functions not completing before the next iteration of the loop. I actually tried both `let` and `const` on `url`, `html`, and even `userNumber`. I completely forgot about `key`. Clearly my understanding of working with asynchronous functions is a little week. Thank you! – Vince Jan 15 '17 at 12:20