0

I am learning react.

I have a simple react app sample that :

  1. Fetch users
  2. Once users are fetched, show their name on a Card

What I'd like to do is to expand this sample. Instead of using a simple list of users, I'd like to use a list of pokemons. What I try to do is :

  1. Fetch the list of pokemon and add in state.pokemons
  2. Show the Card with the pokemon name from state.pokemons
  3. From that list, get the URL to fetch the detail of the given pokemon and add in state.pokemonsDetails
  4. From the state.pokemonsDetails, update the Cards list to show the image of the pokemon.

My problem is: I don't even know how to re-render the Cards list after a second fetch.

My question is: How to update the Cards list after the second fetch?

See my code below:

import React from "react";
import CardList from "../components/CardList";
import SearchBox from "../components/SearchBox"
import Scroll from "../components/Scroll"
import './App.css';


class App extends React.Component{
    constructor(){
        super();
        this.state = {
            pokemons:[],
            pokemonsDetails:[],
            searchfield: ''
        }
    }

    getPokemons = async function(){
        const response = await fetch('https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20');
        const data = await response.json();
        this.setState({pokemons:data.results})

        
      }

    getPokemonDetails = async function(url){
        //fetch function returns a Promise
        const response = await fetch(url);
        const data = await response.json();
        //console.log('getPokemonDetails', data);
        this.setState({pokemonsDetails:data});

    }

    componentDidMount(){
        this.getPokemons();

    }

    onSearchChange = (event) => {
        this.setState({searchfield: event.target.value})
    
    }

    render(){
        const {pokemons, pokemonsDetails, searchfield} = this.state;

        if(pokemons.length === 0){
            console.log('Loading...');
            return <h1>Loading....</h1>
        }else if (pokemonsDetails.length === 0){
            console.log('Loading details...');
            pokemons.map(pokemon => {
                return this.getPokemonDetails(pokemon.url);
            });
            return <h1>Loading details....</h1>
        }else{
            return(
                <div>
                    <h1>Pokedex</h1>
                    <SearchBox searchChange={this.onSearchChange}/>
                    <Scroll>
                        <CardList pokemons={pokemons}/>
                    </Scroll>
                </div>
            );
        }
    }
}

export default App;

Some remarks :

  1. I can see a problem where my Cards list is first created with state.pokemons, then, I would need to update Cards list with state.pokemonsDetails. The array is not the same.
  2. Second problem, I don't even know how to call the render function after state.pokemonsDetails is filled with the fetch. I set the state, but it looks like render is not called every time
  3. More a question than a remark. The way I update my state in getPokemonDetails might be incorrect. I keep only one detail for one given pokemon. How to keep a list of details? Should I use something else than setState to expand pokemonsDetails array?
Meet Bhalodiya
  • 630
  • 7
  • 25
peterphonic
  • 951
  • 1
  • 19
  • 38
  • 1
    If you are just learning react, I'm curious why you aren't using the functional paradigm. I suggest following a modern react tutorial and circling back around to understand class based components after you have a better handle on the fundamentals. – Chad S. Nov 07 '22 at 16:47
  • @ChadS. I am actually following a tutorial that I thought was using up-to-date react. So, from what you said, the code I am using above is not using "functional paradigm"? And what I try to do might be too complex for what i'd to do? – peterphonic Nov 07 '22 at 16:54
  • Please check this [answer](https://stackoverflow.com/questions/53120972/how-to-call-loading-function-with-react-useeffect-only-once) it could help :) – Aymen Jarouih Nov 07 '22 at 17:19
  • 2
    If you want to learn react and you're just starting out, check out the new docs: https://beta.reactjs.org/learn – Chad S. Nov 07 '22 at 19:34

1 Answers1

1

You can combine 2 API calls before pokemons state update that would help you to control UI re-renderings better

You can try the below approach with some comments

Side note that I removed pokemonDetails state, so you won't see the loading elements for pokemonDetails as well

import React from "react";
import CardList from "../components/CardList";
import SearchBox from "../components/SearchBox";
import Scroll from "../components/Scroll";
import "./App.css";

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      pokemons: [],
      searchfield: ""
    };
  }

  getPokemons = async function () {
    const response = await fetch(
      "https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20"
    );
    const data = await response.json();

    //try to get all pokemon details at once with fetched URLs
    const pokemonDetails = await Promise.all(
      data.results.map((result) => this.getPokemonDetails(result.url))
    );

    //map the first and second API response data by names
    const mappedPokemons = pokemonDetails.map((pokemon) => {
      const pokemonDetail = pokemonDetails.find(
        (details) => details.name === pokemon.name
      );
      return { ...pokemon, ...pokemonDetail };
    });

    //use mapped pokemons for UI display
    this.setState({ pokemons: mappedPokemons });
  };

  getPokemonDetails = async function (url) {
    return fetch(url).then((response) => response.json());
  };

  componentDidMount() {
    this.getPokemons();
  }

  onSearchChange = (event) => {
    this.setState({ searchfield: event.target.value });
  };

  render() {
    const { pokemons, searchfield } = this.state;

    if (pokemons.length === 0) {
      return <h1>Loading....</h1>;
    } else {
      return (
        <div>
          <h1>Pokedex</h1>
          <SearchBox searchChange={this.onSearchChange} />
          <Scroll>
            <CardList pokemons={pokemons} />
          </Scroll>
        </div>
      );
    }
  }
}

export default App;

Sandbox


If you want to update pokemon details gradually, you can try the below approach

import React from "react";
import CardList from "../components/CardList";
import SearchBox from "../components/SearchBox";
import Scroll from "../components/Scroll";
import "./App.css";

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      pokemons: [],
      searchfield: ""
    };
  }

  getPokemons = async function () {
    const response = await fetch(
      "https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20"
    );
    const data = await response.json();

    this.setState({ pokemons: data.results });

    for (const { url } of data.results) {
      this.getPokemonDetails(url).then((pokemonDetails) => {
        this.setState((prevState) => ({
          pokemons: prevState.pokemons.map((pokemon) =>
            pokemon.name === pokemonDetails.name
              ? { ...pokemon, ...pokemonDetails }
              : pokemon
          )
        }));
      });
    }
  };

  getPokemonDetails = async function (url) {
    return fetch(url).then((response) => response.json());
  };

  componentDidMount() {
    this.getPokemons();
  }

  onSearchChange = (event) => {
    this.setState({ searchfield: event.target.value });
  };

  render() {
    const { pokemons, searchfield } = this.state;

    if (pokemons.length === 0) {
      return <h1>Loading....</h1>;
    } else {
      return (
        <div>
          <h1>Pokedex</h1>
          <SearchBox searchChange={this.onSearchChange} />
          <Scroll>
            <CardList pokemons={pokemons} />
          </Scroll>
        </div>
      );
    }
  }
}

export default App;

Sandbox

Side note that this approach may cause the performance issue because it will keep hitting API for fetching pokemon details multiple times and updating on the same state for UI re-rendering

Nick Vu
  • 14,512
  • 4
  • 21
  • 31
  • It worked. If I may, I think you have to change `pokemonDetails.map((pokemon) =>` by `data.results.map((pokemon) =>`, otherwise, the spread syntax does nothing, the APIs are not concatenated. Also, do you know documentations that would explain how to update components later? What I mean is, your solution is working great, but if I want to load all pokemons, it takes time. Would be better to load just part of data and refresh on demand. Thank again for your time, really appreciated... – peterphonic Nov 08 '22 at 02:14
  • 1
    My approach is different from yours a bit. Your approach is updating pokemon details one by one which will make re-renderings continuously by the number of returned pokemon details, but with my approach, I put all pokemon details into a promise queue `Promise.all` which will send all requests at once, and wait for all responses returned by `await` that helps to update UI once only. All state updates are async, so you can update UI anytime you want by state update, the problem here is pokemon API isn't designed to get all pokemon details at once, so we need to work around this case @peterphonic – Nick Vu Nov 08 '22 at 02:46
  • 1
    @peterphonic I updated my answer with a gradual update for pokemon details you can check it out, but I'd not recommend that approach in real cases because it may cause performance problems if you don't handle it carefully – Nick Vu Nov 08 '22 at 03:06