Escuela Frontend
React

Data Fetching con React Usando useState y useEffect

Nicolás Gómez
Autor
Nicolás Gómez

Cuando construimos aplicaciones web con React, es común que tengamos que comunicarnos con una fuente de datos de un servidor web (API).

Aquí es donde la API Fetch nos puede ayudar, ya que es una interfaz para recuperar recursos de la red. Pero, ¿Cómo utilizamos fetch en una aplicación React?

Lo que vas a aprender

La meta de este artículo es mostrarte cómo utilizar la API Fetch en React para poder interactuar con una API.

En particular, aprenderás a utilizar los hooks useState y useEffect para gestionar el estado de los datos que obtengas de la API.

Requisitos

Lo que construiremos

Construiremos una aplicación que muestre una imagen de perrito aleatoria obtenida de Dog API:

Al cargar la aplicación vemos un perrito aleatorio.

https://res.cloudinary.com/escuela-frontend/image/upload/v1632282719/articles/data-fetching-react/demo-final_guo8in.gif

Esta aplicación tiene las siguientes funcionalidades:

  • Cuando la abres por primera vez, te muestra un perrito aleatorio.
  • Cuando presionas el botón “Otro ❤️” muestra otro perrito aleatorio.
  • Si ocurre algún problema al obtener los datos del perrito, mostrar el error.

Utilizaremos las herramientas que nos ofrece React (useState, useEffect) y el browser (Fetch API).

La base inicial del proyecto la puedes ver en este Codesandbox, el cual es un proyecto en React en blanco donde el único cambio que hice fue agregar una hoja de estilos.

A partir de este proyecto, puedes ir siguiendo los pasos para construir la aplicación.

Cómo está organizado este artículo

Partiremos con una base conceptual, donde veremos qué es Fetch API, cómo utilizarla y cómo manejar errores. Luego nos vamos a ir a la construcción de la aplicación.

Conceptos previos

¿Qué es “Fetch API”?

Fetch API es una interfaz para obtener información de una URL.

Lo bueno es que la mayoría de los navegadores modernos incluyen la función de manera global, por lo que podemos usarla sin necesidad de instalar librerías adicionales en nuestros proyectos, incluso en proyectos con React.

¿Cómo utilizamos “Fetch API”?

Su uso es relativamente sencillo y lo veremos a continuación (puedes ver más detalles en este excelente artículo de MDN).

La Dog API tiene un endpoint que entrega la imagen de un perro random, el cual es https://dog.ceo/api/breeds/image/random. Para hacer una solicitud a este endpoint utilizando fetch podemos usar el siguiente código:

fetch("https://dog.ceo/api/breeds/image/random")
.then((response) => response.json())
.then((dog) => console.log(dog));

Al ejecutar esto (ver ejemplo en Codesanbox), imprimirá en consola algo parecido a esto:

{
"message": "https://images.dog.ceo/breeds/spaniel-irish/n02102973_2870.jpg",
status": "success"
}

Siempre que usamos fetch a una API que responde en JSON debemos hacer los mismos pasos:

fetch("https://dog.ceo/api/breeds/image/random") // ⬅️ 1) llamada a la API, el resultado es una Promise
.then((response) => response.json()) // ⬅️ 2) cuando la petición finalice, transformamos la respuesta a JSON (response.json() también es una Promise)
.then((dog) => console.log(dog)); // ⬅️ 3) aquí ya tenemos la respuesta en formato objeto

El llamado de fetch retorna un objeto de tipo Promise, es por eso que tenemos que encadenar un then que se ejecutará una vez fetch haya terminado de obtener el recurso solicitado. A su vez, la línea que transforma a JSON la respuesta también es asíncrona, es por eso que cuando usamos fetch necesitaremos tener dos then, uno para la respuesta y la otra para su transformación a JSON.

¿Cómo manejamos errores con Fetch API?

Cuando realizamos peticiones a un servidor, hay muchas razones por las cuales puede fallar. Puede haber un timeout de la API, o quizás el servicio está caído. A continuación veremos cómo manejar estos errores.

Para este ejemplo, usaremos mock.codes, un servicio que permite probar respuestas HTTP de manera simple. Solo tienes que hacer un request a mock.codes/HTTP_CODE y obtendras una respuesta HTTP con el código solicitado. Por ejemplo, mock.codes/500 te responde con error HTTP 500.

Veamos el siguiente ejemplo:

fetch("mock.codes/500")
.then(() => console.log("Todo bien"))
.catch(() => console.log("Algo falló"))

¿Qué crees que imprime lo anterior?

La respuesta es: “Todo bien”.

Esto es un poco contra intuitivo, ya que en realidad la solicitud si falló, pues hubo un error 500. Lo que pasa es que la especificación de fetch dice que la promesa fallará solo si hay fallos de red o si algo impidió completar la solicitud. Para manejar los errores, debemos fijarnos en el atributo ok de la respuesta de fetch, el cual será true para respuestas satisfactorias (códigos 200-299).

Entonces el código debería ser algo así:

fetch('mock.codes/500')
.then((response) => {
if (response.ok) {
console.log('Todo bien');
} else {
console.log('Respuesta de red OK pero respuesta de HTTP no OK');
}
})
.catch((error) => {
console.log('Hubo un problema con la petición Fetch:' + error.message);
})

O usando la sintaxis de async/await:

// ↘️ para usar await debemos hacerlo dentro de una función declarada como "async"
async function fetchExample() {
try {
const response = await fetch('mock.codes/500');
if (response.ok) {
console.log('Todo bien');
} else {
console.log('Respuesta de red OK pero respuesta de HTTP no OK');
}
} catch (error) {
console.log('Hubo un problema con la petición Fetch:' + error.message);
}
}
fetchExample();

Mirando el atributo ok de la respuesta podemos saber si todo salió bien.

Cómo realizar peticiones en React con useEffect

Partiremos implementando la primera funcionalidad:

Cuando abres la aplicación por primera vez, te muestra un perrito aleatorio.

Visualmente:

Al cargar la página, aparece un perrito

https://res.cloudinary.com/escuela-frontend/image/upload/v1632282719/articles/data-fetching-react/perritos-1_cgjfwr.gif

De aquí surge una duda: ¿cómo hacemos que nuestra función espere el resultado de fetch?

Lo que se suele hacer es partir nuestro componente en modo “cargando”, y cuando el resultado de la petición esté listo, actualizamos nuestro componente y mostramos la información que recibimos.

Casi siempre son los mismos pasos, por lo que te presentaré una receta de lo que debemos hacer para lograr esto:

  1. Inicia tu componente en modo “cargando”
  2. Luego de que tu componente haya renderizado, haz la petición de datos.
  3. Cuando la petición de datos haya finalizado, guarda los datos y desactiva el modo “cargando”.
  4. Asegúrate que cuando tu componente esté en modo cargando muestre un indicador de cargando (un spinner o un texto), y cuando no, que muestre los datos que recibió.

¿Cómo hacemos esto en código?

1) Inicia tu componente en modo “cargando”

Vamos a necesitar una forma de decirle al componente que se encuentra en modo cargando, esto lo podemos hacer con una variable de estado.

Así que podemos tener una primera versión que cumpla con este primer punto:

import { useState } from "react";
import "./styles.css";
export default function App() {
const [isLoading, setIsLoading] = useState(true);
return (
<div className="App">
<h1>Cargando...</h1>
</div>
);
}

2) Luego de que tu componente haya renderizado, haz la petición de datos.

Esto es exactamente lo mismo que decir “utiliza useEffect para obtener los datos”, porque ya sabemos que las peticiones de datos debemos hacerlo en un useEffect, y además, la función que le pasamos a useEffect se ejecuta luego de haber renderizado el componente.

Así que el código nos queda de la siguiente manera:

import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch("https://dog.ceo/api/breeds/image/random")
.then((response) => response.json())
.then((dog) => console.log(dog));
}, []);
return (
<div className="App">
<h1>Cargando...</h1>
</div>
);
}

Notar que el segundo argumento de useEffect es un arreglo vacío ([]). Esto lo hago así porque la idea es ejecutar el efecto (llamada a la API) una sola vez (luego del primer renderizado). Si omitimos esto, cada vez que se renderice el componente se volverá a ejecutar el useEffect.

Nota: puedes leer más sobre la importancia del arreglo de dependencias aquí.

3) Cuando la petición de datos haya finalizado, guarda los datos y desactiva el modo “cargando”.

La petición de datos finaliza en la línea donde estamos haciendo el console.log de la respuesta. Así que es ahí donde debemos guardar los datos y desactivar el modo cargando:

useEffect(() => {
fetch("https://dog.ceo/api/breeds/image/random")
.then((response) => response.json())
.then((dog) => {
// ⬅️ aquí debemos guardar los datos y desactivar el modo cargando
});
}, []);

Para guardar los datos utilizaremos una nueva variable de estado llamada imageUrl, donde guardaremos la URL de la imagen del perrito a mostrar:

// ...
const [imageUrl, setImageUrl] = useState(null);
// ...

Así que aplicando esto:

import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [isLoading, setIsLoading] = useState(true);
const [imageUrl, setImageUrl] = useState(null);
useEffect(() => {
fetch("https://dog.ceo/api/breeds/image/random")
.then((response) => response.json())
.then((dog) => {
setImageUrl(dog.message); // ⬅️ Guardar datos
setIsLoading(false); // ⬅️ Desactivar modo "cargando"
});
}, []);
return (
<div className="App">
<h1>Cargando...</h1>
</div>
);
}

4) Mostrar cargando o los datos cuando corresponda

Hasta ahora no nos hemos preocupado del renderizado de nuestro componente. Eso cambiará en unos segundos.

La primera parte de este paso dice: “Asegúrate que cuando tu componente esté en modo cargando muestre un indicador de cargando”.

Eso lo podemos hacer de la siguiente manera:

import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [isLoading, setIsLoading] = useState(true);
const [imageUrl, setImageUrl] = useState(null);
useEffect(() => {
fetch("https://dog.ceo/api/breeds/image/random")
.then((response) => response.json())
.then((dog) => {
setImageUrl(dog.message);
setIsLoading(false);
});
}, []);
if (isLoading) { // ⬅️ si está cargando, mostramos un texto que lo indique
return (
<div className="App">
<h1>Cargando...</h1>
</div>
);
}
return (
<div className="App">
<h1>Listo</h1>
</div>
);
}

Como puedes ver, el renderizado ahora es condicional. Cuando está cargando (isLoading es true) veremos un “Cargando…”, mientras que cuando terminó de cargar veremos un “Listo”.

La segunda parte de este paso dice: “y cuando no, que muestre los datos que recibió”. Para esto, en vez de mostrar “Listo”, debemos mostrar la imagen con una etiqueta img:

return (
<div className="App">
<img src={imageUrl} alt="Imagen de perrito aleatoria"/>
</div>
);

El código final para nuestra primera funcionalidad:

import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [isLoading, setIsLoading] = useState(true);
const [imageUrl, setImageUrl] = useState(null);
useEffect(() => {
fetch("https://dog.ceo/api/breeds/image/random")
.then((response) => response.json())
.then((dog) => {
setImageUrl(dog.message);
setIsLoading(false);
});
}, []);
if (isLoading) {
return (
<div className="App">
<h1>Cargando...</h1>
</div>
);
}
return (
<div className="App">
<img src={imageUrl} alt="Imagen de perrito aleatoria" />
</div>
);
}

¡Listo! 😄 Nuestra aplicación de perritos aleatorios está funcionando.

Estos 4 pasos son los típicos que tendremos que hacer cuando queramos hacer una aplicación donde queramos mostrar algún dato que vive en una API. Funciona tanto para obtener datos individuales o lista de datos.

Cómo realizar peticiones a partir de eventos del usuario

Ahora implementaremos la segunda funcionalidad:

Cuando presionas el botón “Otro ❤️” muestra otro perrito aleatorio.

Así que ahora agregaremos un botón que al darle clic recargue la imagen del perrito que mostramos. Partiremos mostrando el botón:

import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [isLoading, setIsLoading] = useState(true);
const [imageUrl, setImageUrl] = useState(null);
useEffect(() => {
fetch("https://dog.ceo/api/breeds/image/random")
.then((response) => response.json())
.then((dog) => {
setImageUrl(dog.message);
setIsLoading(false);
});
}, []);
if (isLoading) {
return (
<div className="App">
<h1>Cargando...</h1>
</div>
);
}
return (
<div className="App">
<img src={imageUrl} alt="Imagen de perrito aleatoria" />
<button>{/* ⬅️ nuevo */}
¡Otro!{" "}
<span role="img" aria-label="corazón">
❤️
</span>
</button>
</div>
);
}

Lo siguiente a implementar es que cuando hagan clic en el botón, debemos recargar la imagen. Si lo piensas bien, esto es lo mismo que hacemos en el useEffect, solo que ahora debemos hacerlo en respuesta al clic del nuevo botón.

Vamos a crear la función que se ejecutará al darle clic al botón:

// ...
const fetchRandomDog = () => {
// ⬅️ aquí haremos una solicitud a la API
};
// ...
<button onClick={fetchRandomDog}>

La implementación de fetchRandomDog podría ser muy parecido a lo que hay en el useEffect:

const fetchRandomDog = () => {
setIsLoading(true); // ⬅️ esto es nuevo (comparando con lo que hay en el useEffect)
fetch("https://dog.ceo/api/breeds/image/random")
.then((response) => response.json())
.then((dog) => {
setImageUrl(dog.message);
setIsLoading(false);
});
};

Esto nos deja el código final así:

import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [isLoading, setIsLoading] = useState(true);
const [imageUrl, setImageUrl] = useState(null);
useEffect(() => {
fetch("https://dog.ceo/api/breeds/image/random")
.then((response) => response.json())
.then((dog) => {
setImageUrl(dog.message);
setIsLoading(false);
});
}, []);
const fetchRandomDog = () => { /* ⬅️ función para obtener un perrito aleatorio */
setIsLoading(true);
fetch("https://dog.ceo/api/breeds/image/random")
.then((response) => response.json())
.then((dog) => {
setImageUrl(dog.message);
setIsLoading(false);
});
};
if (isLoading) {
return (
<div className="App">
<h1>Cargando...</h1>
</div>
);
}
return (
<div className="App">
<img src={imageUrl} alt="Imagen de perrito aleatoria" />
<button onClick={fetchRandomDog}> {/* ⬅️ llamarla cuando hagamos clic */}
¡Otro!{" "}
<span role="img" aria-label="corazón">
❤️
</span>
</button>
</div>
);
}

Lo cual funciona de maravilla.

Como verás, hay código duplicado en useEffect y fetchRandomDog. Hay varias formas de refactorizar esto, aunque en este caso particular mi favorita es dejar a useEffect como el principal encargado de hacer la petición.

¿Qué pasa si modificamos useEffect de la siguiente manera?

useEffect(() => {
if (isLoading) { // ⬅️ solo hacer request si estamos en modo "cargando"
fetch("https://dog.ceo/api/breeds/image/random")
.then((response) => response.json())
.then((dog) => {
setImageUrl(dog.message);
setIsLoading(false);
});
}
}, [isLoading]); // ⬅️ ahora este efecto se ejecutará cada vez que cambie este estado

El efecto se ejecutará cada vez que isLoading cambie, aunque solo hará la petición si isLoading es true.

Si pensamos en el primer renderizado, sabemos que isLoading parte como true, entonces este useEffect funcionará igual que antes

Gracias a este cambio, cuando hacemos clic en el botón basta con cambiar el estado de isLoading a true 😎

const fetchRandomDog = () => {
setIsLoading(true);
};

La versión refactorizada nos queda así:

import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [isLoading, setIsLoading] = useState(true);
const [imageUrl, setImageUrl] = useState(null);
useEffect(() => {
if (isLoading) { // ⬅️ solo hacer la solicitud si isLoading = true
fetch("https://dog.ceo/api/breeds/image/random")
.then((response) => response.json())
.then((dog) => {
setImageUrl(dog.message);
setIsLoading(false);
});
}
}, [isLoading]); // ⬅️ ahora este efecto se ejecutará cada vez que cambie este estado
const randomDog = () => {
setIsLoading(true); // ⬅️ simplemente actualizamos isLoading a true
};
if (isLoading) {
return (
<div className="App">
<h1>Cargando...</h1>
</div>
);
}
return (
<div className="App">
<img src={imageUrl} alt="Imagen de perrito aleatoria" />
<button onClick={randomDog}>
¡Otro!{" "}
<span role="img" aria-label="corazón">
❤️
</span>
</button>
</div>
);
}

De esta forma, cuando nuestro componente esté en modo “cargando” sabremos que estará haciendo una petición. Y de pasada, nos ahorramos duplicar código.

¿Cómo manejar errores HTTP en React?

Implementaremos la última funcionalidad:

Si ocurre algún problema al obtener los datos del perrito, mostrar el error.

Cuando llamamos fetch, debemos preocuparnos de los posibles errores, los cuales son dos:

  1. Algún error que impida realizar la solicitud.
  2. Algún error de la API.

El primero hace que la Promise retornada por fetch falle, mientras que el segundo no. Sin embargo, en este caso podemos verificar la propiedad ok de la respuesta para saber si todo salió bien.

Por todo esto, un useEffect más robusto, con manejo de estos dos errores lo podríamos implementar así:

//...
const [error, setError] = useState(null); // ⬅️ nueva variable de estado para guardar un mensaje de error
//...
useEffect(() => {
if (isLoading) {
fetch("https://dog.ceo/api/breeds/image/random")
.then((response) => {
if (response.ok) { // ⬅️ verificamos que todo esté bien con la respuesta HTTP
response.json().then((dog) => { // ⬅️ si está todo bien, proseguimos a transformar a JSON y actualizar los estados
setImageUrl(dog.message);
setError(null);
setIsLoading(false);
});
} else {
setError("Hubo un error al obtener el perrito"); // ⬅️ hubo un problema HTTP 4XX o 5XX
}
})
.catch((error) => { // ⬅️ hubo un problema que no permitió hacer la solicitud
setError("No pudimos hacer la solicitud para obtener el perrito");
});
}
}, [isLoading]);

El código se vuelve más complejo, pero más robusto. Alternativamente podríamos usar async y await:

useEffect(() => {
if (isLoading) {
async function fetchData() {
try {
const response = await fetch(
"https://dog.ceo/api/breeds/image/random"
);
if (response.ok) {
const dog = await response.json();
setImageUrl(dog.message);
setError(null);
setIsLoading(false);
} else {
setError("Hubo un error al obtener el perrito");
}
} catch (error) {
setError("No pudimos hacer la solicitud para obtener el perrito");
}
}
fetchData();
}
}, [isLoading]);

Solo nos falta mostrar el mensaje de error (si es que existe). El código final nos queda así:

import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [isLoading, setIsLoading] = useState(true);
const [imageUrl, setImageUrl] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
if (isLoading) {
async function fetchData() {
try {
const response = await fetch(
"https://dog.ceo/api/breeds/image/random"
);
if (response.ok) {
const dog = await response.json();
setImageUrl(dog.message);
setError(null);
setIsLoading(false);
} else {
setError("Hubo un error al obtener el perrito");
}
} catch (error) {
setError("No pudimos hacer la solicitud para obtener el perrito");
}
}
fetchData();
}
}, [isLoading]);
const randomDog = () => {
setIsLoading(true);
};
if (isLoading) {
return (
<div className="App">
<h1>Cargando...</h1>
</div>
);
}
if (error) { // ⬅️ mostramos el error (si es que existe)
return (
<div className="App">
<h1>{error}</h1>
<button onClick={randomDog}>Volver a intentarlo</button>
</div>
);
}
return (
<div className="App">
<img src={imageUrl} alt="Imagen de perrito aleatoria" />
<button onClick={randomDog}>
¡Otro!{" "}
<span role="img" aria-label="corazón">
❤️
</span>
</button>
</div>
);
}

Recapitulación

En este artículo vimos como crear una aplicación que consume datos de una API para luego mostrarlos, utilizando elementos nativos de React (useState, useEffect) y de los browsers modernos (Fetch API).

Para esto, presentamos la receta típica que debemos seguir:

  1. Inicia tu componente en modo “cargando”.
  2. Luego de que tu componente haya renderizado, haz la petición de datos.
  3. Cuando la petición de datos haya finalizado, guarda los datos y desactiva el modo “cargando”.
  4. Asegúrate que cuando tu componente esté en modo cargando muestre un indicador de cargando (un spinner o un texto), y cuando no, que muestre los datos que recibió.

También vimos cómo hacer solicitudes luego de ciertos eventos (por ejemplo, luego de un clic). Finalmente cómo manejar errores, ya sea errores relacionados con API o con la solicitud.

Artículos Relacionados

¿Quieres mejorar tus habilidades de frontend?