简体   繁体   中英

Why doesn't my React component renders what I want in a foreach, but will console.log the array?

I'm following an online course about React, and I'm practicing with an API ( swapi.co ). The goal is to play around with the API and make a React app that does whatever we want with it.

So I'm trying to make an app that has an input, and everytime the input changes, it fetches the matching character through the API and displays data about the character. One of the data is the name of the films the character was in, so it's an array containing one or many strings. Everything goes well until I try to loop through that array to display. It will console.log the whole array, but won't loop through it with foreach.

Here's my result component :

class ResultPeople extends Component {
constructor() {
    super()
    this.state = {
        movieTitles: []
    }
}
getMovies(movies) {
    var movieTitleList = [];
    movies.forEach(element => {
        fetch(element)
        .then(resp => resp.json())
        .then(movieData => {
            movieTitleList.push(movieData.title);
        })
    });
    this.setState({movieTitles: movieTitleList});
    console.log('getMovies Done');
}
render() {
    const { resultdata } = this.props;
    const { name, films } = resultdata;
    return (
        <div className="result-characters text-white">
            <h2>{name}</h2>
            {films ? <button onClick={() => this.getMovies(films)}>See movies</button> : ''}
            {
                this.state.movieTitles ? <MovieList movies={this.state.movieTitles} /> : null
            }
        </div>
    );
}

}

And here's the movie list :

const MovieList = ({ movies }) => {
return (
    <ul>
        {console.log(movies)}
        {
            movies.forEach(element => {
                return <li>{element}</li>;
            })
        }
    </ul>
);

}

I have no clue as to why it behaves that way. console.log(movies) works well ( though depending on the times it logs a different amount of films, perhaps because of the API ? ). But the foreach doesn't.

Where's the mistake here ? Thanks !

You make a series of async calls that return promises, and you need to wait for all of them to finish before setting the state.

Iterate the movies with Array.map() make the fetch calls, and return the promises. Wait for all promises to finish using Promise.all() , and then set the state.

Example (not tested):

getMovies(movies) {
  Promise.all(movies.map(element =>
    fetch(element)
    .then(resp => resp.json())
    .then(movieData => movieData.title)
  )).then(movieTitles => this.setState({ movieTitles }))
}

Use Array.map() in your component as well, since you can return from Array.forEach() .

Note: the key should be unique. In this example, I use the element itself, but if they're not unique, you'll need to generate a unique key.

const MovieList = ({ movies }) => (
  <ul>
  {
    movies.map(element => (
      <li key={element}>{element}</li>;
    ))
  }
  </ul>
);

It's a common mistake (I also trapped into it several times) - use .map!

I mean there is forEach in MovieList component.

Also you have initial movieTitles: [] array, and this.state.movieTitles ? check will be always true. Setting null or removing this check is ok.

The call you are making in the foreach loop is async while the function itself is synchronous. I would probably write it as a map function that returns a set of promises that when resolved sets the state.

...
async getMovies(movies) {
    const promises = movies.map(async (element) => {
        const response = await fetch(element)
        const json = await response.json()

        return json.title
    })

    const titles = await Promise.all(promises)

    this.setState({movieTitles: titles});
}
...

The async / await is just a shorter form, you can accomplish the same with then / catch. I would also add some more exception handling and possibly return a falsy value when it fails to filter out the invalid movie titles.

You should be using Array.prototype.map() not Array.prototype.forEach()

> map() returns a new array based on the callback you provide (which is what you want)

{movies.map(element => (<li>{element}</li>)}

> forEach() runs the code in the callback, but then returns undefined.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM