Hasta este momento, todos los datos de nuestra aplicación (listas de usuarios, productos, tareas) estaban “harcodeados” en arrays dentro del código.
Pero el mundo real no funciona así. En el mundo real, los datos viven en un servidor (Backend), en una Base de Datos, y nuestra aplicación React debe pedirlos, esperar a que lleguen y mostrarlos.
Para comunicarnos con ese servidor, el navegador nos ofrece una herramienta nativa estándar: la Fetch API.
En este artículo aprenderemos a integrar fetch dentro del ciclo de vida de React para hacer peticiones GET.
¿Dónde se hacen las peticiones?
¿Puedo poner el fetch directamente en el cuerpo del componente? ❌ NO.
function MalEjemplo() {
// ¡ERROR GRAVE!
// Esto se ejecutará en CADA renderizado.
fetch('https://api.com/datos').then(...)
return <div>...</div>
}
Una petición HTTP es un Efecto Secundario. No forma parte del cálculo puro de la interfaz. Por tanto, el único lugar correcto para hacer una petición al montar el componente es dentro de useEffect.
El Patrón Loading, Error, Data
Cuando pedimos datos a un servidor, la respuesta no es inmediata. La aplicación pasa por tres estados lógicos que debemos gestionar visualmente:
- Loading: “Estoy esperando los datos”. (Mostrar spinner).
- Error: “Algo ha fallado”. (Mostrar mensaje de error).
- Data: “Aquí están los datos”. (Mostrar la lista).
Para gestionar esto, necesitamos tres estados (o un objeto de estado que los agrupe).
const [data, setData] = useState(null); // Los datos finales
const [loading, setLoading] = useState(true); // ¿Estamos cargando?
const [error, setError] = useState(null); // ¿Hubo error?
Implementación paso a paso
Vamos a crear un componente que pida una lista de usuarios a una API pública (usaremos JSONPlaceholder).
La estructura del useEffect
Aquí hay un truco técnico importante. La función que pasamos a useEffect no puede ser asíncrona (no puede devolver una Promesa).
Por eso, debemos crear una función async interna y llamarla inmediatamente.
import { useState, useEffect } from "react";
export default function ListaUsuarios() {
const [users, setUsers] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Definimos la función asíncrona dentro del efecto
const fetchData = async () => {
try {
setLoading(true); // Iniciamos carga (opcional si el estado inicial ya es true)
const response = await fetch("https://jsonplaceholder.typicode.com/users");
// OJO: Fetch no lanza error en 404 o 500, solo en fallos de red.
// Debemos comprobar response.ok manualmente.
if (!response.ok) {
throw new Error(`Error HTTP: ${response.status}`);
}
const jsonData = await response.json();
setUsers(jsonData);
setError(null); // Limpiamos errores previos si los hubiera
} catch (err) {
setError(err.message);
setUsers(null);
} finally {
setLoading(false); // Tanto si va bien como mal, terminamos de cargar
}
};
// Ejecutamos la función
fetchData();
}, []); // <--- Array vacío: IMPORTANTE para que solo se ejecute al montar
Renderizado condicional
Ahora usamos los patrones de Early Return para decidir qué pintar.
// Bloque de Carga
if (loading) {
return <div className="spinner">Cargando usuarios...</div>;
}
// Bloque de Error
if (error) {
return (
<div className="alert-error">
Ocurrió un error: {error}
<button onClick={() => window.location.reload()}>Reintentar</button>
</div>
);
}
// Bloque de Éxito (Data)
return (
<ul>
{users.map((user) => (
<li key={user.id}>
<strong>{user.name}</strong> ({user.email})
</li>
))}
</ul>
);
}
El problema de Fetch nativo
Aunque fetch funciona bien, tiene algunas incomodidades que los desarrolladores suelen odiar:
- No lanza errores en 404/500: Tienes que escribir el
if (!response.ok)siempre. - Dos pasos: Tienes que hacer
fetchy luego.json(). - Boilerplate: Tienes que escribir manualmente todos los
headers(Content-Type, Tokens de Auth, etc.) en cada petición.
Por estas razones, en aplicaciones grandes suele interesar envolver fetch en una pequeña capa propia, o usar un cliente HTTP más cómodo.
