Understanding async in React components

Viewed 88

I'm using axios to return the details of a Pokemon:

class PokeDetails extends React.Component{
    constructor(props){
        super(props);
        this.state = { pokemon: null }
    }
    componentDidMount(){
        axios.get('https://pokeapi.co/api/v2/pokemon/1')
        .then(res => {
            this.setState({
                pokemon: res.data
            });
        }).catch((err) => { console.log('Axios Poke Details error: ', err) });
    }
    render(){
        const {pokemon} = this.state;
        const pokeCard = pokemon ? (
            <div className="poke-details">
               <img src={pokemon.sprites.front_default} alt={`${pokemon.name} front`}/>
               <h3 className="card-title">{pokemon.name}</h3>
            </div>
            ) : (
            <div className="center">Loading Pokemon...</div>)
        return(
            <div className="container">
                {pokeCard}
            </div>
        )
    }
}

export default PokeDetails

I want to display the pokemon types, which is an array that has a length of 1 or 2 depending on the pokemon. So I thought:

render(){
   const {pokemon} = this.state
   const listTypes = pokemon.types.map((type) => { <li>{type.name}</li> });
}

...and render listTypes in a ul in const pokeCard. When I do this I get an error saying pokemon is null. Shouldn't this not happen because of the ternary operator rendering the pokeCard?

3 Answers

Using pokemon before its detail get fetched from service, try like given:

const listTypes = pokemon && pokemon.types.map((type) => { <li>{type.name}</li> });

Problem is that componentDidMount is called after the initial render. So on the initial render, pokemon is null. That is why accessing pokemon.types fails.

You need to access pokemon.types only when pokemon is not null. Just move the code where you .map() over the pokemon.types inside the ternary operator.

const pokeCard = pokemon ? (
  <div className="poke-details">
    <img src={pokemon.sprites.front_default} alt={`${pokemon.name} front`} />
    <h3 className="card-title">{pokemon.name}</h3>
    <ul>
     {pokemon.types.map(({ type }) => (
       <li key={type.name}>{type.name}</li>
     ))}
    </ul>
  </div>
) : (
  <div className="center">Loading Pokemon...</div>
);

Demo:

class PokeDetails extends React.Component {
  constructor(props) {
    super(props);
    this.state = { pokemon: null };
  }
  
  componentDidMount() {
    axios
      .get("https://pokeapi.co/api/v2/pokemon/1")
      .then((res) => {
        this.setState({
          pokemon: res.data
        });
      })
      .catch((err) => {
        console.log("Axios Poke Details error: ", err);
      });
  }
  
  render() {
    const { pokemon } = this.state;
    const pokeCard = pokemon ? (
      <div className="poke-details">
        <img
          src={pokemon.sprites.front_default}
          alt={`${pokemon.name} front`}
        />
        <h3 className="card-title">{pokemon.name}</h3>
        <ul>
          {pokemon.types.map(({ type }) => (
            <li key={type.name}>{type.name}</li>
          ))}
        </ul>
      </div>
    ) : (
      <div className="center">Loading Pokemon...</div>
    );
    return <div className="container">{pokeCard}</div>;
  }
}

ReactDOM.render(<PokeDetails />,document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.20.0/axios.min.js"></script>
<div id="root"></div>

Rome data is better kept in cache, in case it gets shared between componenets in different levels. I reproduced your code:

import React from 'react';
import useSWR from 'swr';

const fetcher = async (...args) => {
  const res = await fetch(...args);

  return res.json();
};

function PokeDetails() {
  const { data, error } = useSWR('https://pokeapi.co/api/v2/pokemon/1', fetcher);

  if (error) return <div>failed to load</div>;
  if (!data) return <div>loading...</div>;

  return (
    <div>
      {data ? (
        data.map((pokemon) => {
          return (
            <div className='poke-details'>
              <img
                src={pokemon.sprites.front_default}
                alt={`${pokemon.name} front`}
              />
              <h3 className='card-title'>{pokemon.name}</h3>
            </div>
          );
        })
      ) : (
        <div>Some loading content</div>
      )}
    </div>
  );
}

export default PokeDetails;

you can check this article to get a clear idea.

Related