peterphonic
peterphonic

Reputation: 1039

How to update a react component after a fetch

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?

Upvotes: 0

Views: 2849

Answers (1)

Nick Vu
Nick Vu

Reputation: 15520

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

Upvotes: 1

Related Questions