Obtener datos con Axios es fácil, pero mostrarlos en React ~OMFG~

Es probable que sepas cómo obtener datos con Axios, ¿pero cómo los obtienes y los muestras con React?

Quizás probaste algo como usar await en la función render de tu componente, de manera de que React esperara por tu request antes de renderizarse. Pero cuando haces algo así:

async render() {
  return <>{await axios.get(..)}</>
}
await in render()

Obtienes el siguiente error:

Error when using await in render()

¿Por qué ocurre esto? Porque el operador async hace que la función render retorne una Promise, la cual es un objeto y React no sabe cómo renderizar objetos.

Entonces, ¿cómo hacemos que React espere obtener datos de una API antes del render?

Bueno, la respuesta es: haciendo trampa 😏

Esperando a Axios para renderizar

La receta para que React "espere" la respuesta de Axios es siempre la misma:

  • Comienza tu componente en modo "loading".
  • Cuando tu componente se "monte", haz el request.
  • Cuando el request finalice, guarda los datos y apaga el modo "loading".
  • Asegúrate mostrar un indicador de "cargando" o similar mientras tu componente está en modo "loading", y cuando no, mostrar tus datos,

¿Cómo hacemos esto en código?

Mira la siguiente APP en React. Esta APP obtiene datos sobre un Pokémon particular y lo muestra:

Example code can be found here

Aquí está la implementación:

function App() {
  const [isLoading, setLoading] = useState(true);
  const [pokemon, setPokemon] = useState();

  useEffect(() => {
    axios.get("https://pokeapi.co/api/v2/pokemon/4").then(response => {
      setPokemon(response.data);
      setLoading(false);
    });
  }, []);

  if (isLoading) {
    return <div className="App">Loading...</div>;
  }

  return (
    <div className="App">
      <h1>{pokemon.name}</h1>
      <img alt={pokemon.name} src={pokemon.sprites.front_default} />
    </div>
  );
}
Load, fetch and display!

¿Cómo se traduce este código a la receta?

1) Comienza tu componente en modo "loading"

Así, cuando parte el componente, mostrará el texto "Loading..." (gracias a que isLoading parte en true). Esto es equivalente a decir que nuestro componente está en modo loading (o cargando).

¿Por qué usar setState en vez de un simple const? Porque queremos que el componente recuerde el valor!

2) Cuando tu componente se "monte", haz el request

Se monta → "after render" → se logra con useEffect

useEffect recibe una función que será llamada después que el DOM (lo que el browser nos muestra) se actualiza. Aquí le decimos a Axios que obtenga los datos de Charmander de la PokeApi.

Cuando se ejecuta axios.get, no ocurre nada visualmente debido a que este llamado es asincrónico.

3) Cuando el request finalice, guarda los datos y apaga el modo "loading".

Cuando el request finalizó (cuando se ejecuta la función que le pasamos a get con then), actualizamos el estado del componente consetLoading y setPokemon.

Estos son 2 llamados, y ambos actualizan variables de estado distintas:

  • setPokemon actualiza pokemon  de undefined a response.data
  • setLoading actualiza isLoading de true a false

Estos dos llamados hacen que ejecuten 2 renders.

El segundo render  (provocado por  setPokemon) no será visible en el browser porque isLoading sigue siendo true, por lo tanto el componente imprime "Loading..." nuevamente.

El tercer render (provocado por setLoading) mostrará a Charmander en el browser! Esto porque esta vez isLoading es false, y en consecuencia llegamos al final de la función donde el pokemon es mostrado.

El orden es importante. Llamar setLoading antes de setPokemon resultará en un error porque el render final (cuando isLoading === false) asume que pokemon existe. Por eso llamamos setLoading después de setPokemon

Resumen de los renders:

Orden en el cual se llaman las cosas

¿No sabes hooks?

Hey! Es 2020, deberías! 😂

Bueno... aquí está el equivalente usando clases:

export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = { isLoading: true, pokemon: undefined };
  }

  componentDidMount() {
    console.debug("After mount! Let's load data from API...");
    axios.get("https://pokeapi.co/api/v2/pokemon/4").then(response => {
      this.setState({ pokemon: response.data });
      this.setState({ isLoading: false });
    });
  }

  render() {
    const { isLoading, pokemon } = this.state;

    if (isLoading) {
      return <div className="App">Loading...</div>;
    }

    return (
      <div className="App">
        <h1>{pokemon.name}</h1>
        <img alt={pokemon.name} src={pokemon.sprites.front_default} />
      </div>
    );
  }
}
Código con clases, codesandbox link