3

I'm trying to learn the basics of a full stack React app and I'm running into a really weird issue with my API calls.

I want to retrieve a list of trailers from a database that get returned as an array of objects. I then use a promise to loop over that array, and make a separate API call on to each item in the array to add a key value pair to the object. After I've built that array of objects, I set it to my app state, then pass it down through a prop to the child component that should render a list of elements based on the info in the array.

Right now, the console logs the array the component receives and all the data is present, however, every field can be rendered except the one that is added within the forEach loop.

I have removed the API call from the forEach loop, and instead just set a static value in the loop, and the information gets rendered properly as expected. So I'm sure the issue is with the API call being in the loop. I can't figure out why though. The log shows that array is complete, so it doesn't seem to make sense that the render can't find the data.

// The API call

loadUserAndTrailers = () => {
        let trailers = []; 
        axios.get("http://localhost:5000/api/trailers")
        .then (res => {
            trailers = res.data;
        })
        .then (() => {          
            trailers.forEach(trailer => {

 axios.get(`http://localhost:5000/api/votes/user/${this.state.user.id}/${trailer.TrailerID}`)
                .then (res => {
                    const vote = res.data[0].Vote;
                    trailer.vote = vote;
                })
            })
        })
        .then (() => {
            this.setState({trailers: trailers});
        })
}

// The child component

const TrailerList = props => {

    console.log(props)

    const trailers = props.trailers.map(trailer => {
        return <div>{trailer.vote}</div>
    });

    return <div>{trailers}</div>;
};

// The return in the parent component

return (
  <div className="container">
    <div className="content">
      <h1>Trailers</h1>
      <div className="trailers">
      <TrailerList trailers={this.state.trailers}></TrailerList>
    </div>
  </div>
</div>
);

The console.log(props) shows me a completed array with an array of objects with this key value existing { vote: 1 } yet it renders nothing. How can this be? I've been slamming my head against a wall for two days now, but I'm admittedly new to APIs and promises. I was under the impression the promise should make sure the call is completed before moving on to the next step, and my properly logged state seems to imply that that aspect of the code is functioning as expected.

Dale Spiteri
  • 767
  • 1
  • 9
  • 21

1 Answers1

4

The console.log(props) shows me a completed array with an array of objects with this key value existing { vote: 1 } yet it renders nothing. How can this be?

Timing. You launch the loop, make the calls, then don't wait till they finish; your setState will encounter unfilled trailers, and then some time later axios will start filling them in.

A thing to know about console.log (in Chrome at least) is that it is also asynchronous in rendering of objects. Any strings you log will be logged as-is; but objects take time to draw, and by the time the browser gets around to it, it might be drawing the object as it is at the time of drawing, not at the time of logging. So what you're seeing is not what setState saw. See Is Chrome's JavaScript console lazy about evaluating arrays? Use JSON.stringify or pick out primitive values to log what's really there.

The thing you need to do is make sure the then after the loop comes after the loop (i.e. when all the results are done). To do that, there are two things that need to happen.

1) Right now, the axios promise in each iteration of the loop (with its then chain) is being discarded - nothing is waiting on it. If we return it, and make forEach into a map, we'd get an array of promises.

2) To wait for all of the promises in an array, use Promise.all.

So...

.then (() =>
  Promise.all(trailers.map(trailer =>
    axios.get(`${baseUrl}/${this.state.user.id}/${trailer.TrailerID}`)
    .then(res => {
      const vote = res.data[0].Vote;
      trailer.vote = vote;
    })
  ))
})
Amadan
  • 191,408
  • 23
  • 240
  • 301
  • It seems to work. It still seems like a little bit of black magic, and I had a really weird issue pop up. In your `Promise.all(trailers.map(trailer =>` you don't use a curly brace after. Force of habit made me use them when writing mine. It didn't work. Once I removed them. It all seemed to work.Why would a curly brace break the promise functionality? Also, just to clarify, a promise is discarded if the internal function doesn't return a value? – Dale Spiteri Jul 10 '19 at 23:25
  • Last question first: in promise chaining, you have to actually have a chain. If you break it somewhere, you will not wait on the end result, you will only wait till the point where the chain broke. That makes sense, right? As for the curlies: the arrow functions have two forms: `params => expression` or `params => { statements }`. The first one implicitly returns. E.g. `(a, b) => a + b` is equivalent to `(a, b) => { return a + b; }`. – Amadan Jul 11 '19 at 00:37
  • @Amadan Yes, I think I understand. Would my first promise actually be incorrect then? `.then ( res => { trailers = res.data; } )`. I see why it wouldn't break the code, because it's not asynchronous, but I really should be returning to properly chain them, right? Could I alternatively just add an empty return at the end of that process? Like this: `.then ( res => { trailers = res.data; return; } )`, or more neatly, now that you've explained it, like this: `.then ( res => trailers = res.data; )`. I just never thought to return values when the return isn't adding any noticeable functionality. – Dale Spiteri Jul 11 '19 at 15:38
  • Any time you don't have a return from a function, `undefined` is implicitly returned (except for expression-style arrow), just like when you just do a bare `return` without a value. Thus, your two snippets in the last comment are identical in effect. They are both correct; and by "chaining" I meant promise chains; this would not affect them. – Amadan Jul 12 '19 at 00:40