Para obtener datos en React, inicia la petición dentro de useEffect, guarda la respuesta en el estado y renderiza un mensaje de carga hasta que llegue. Pon los valores de los que depende la petición, como un id, en el array de dependencias para que el efecto vuelva a pedir los datos cuando cambien.
Los ejemplos de esta página no pueden acceder a la red, así que fetchUser es una API falsa: una promesa que se resuelve después de un retraso, igual que hace fetch. Cambia 800 por 3000 para que el texto de carga se quede más tiempo.
Con una API real, el cuerpo del efecto se ve así:
useEffect(() => {
fetch('https://api.example.com/users/1')
.then((res) => res.json())
.then((data) => setUser(data));
}, []);
Carga, error y datos
Una petición tiene tres resultados que la pantalla debe mostrar: todavía cargando, fallida o terminada. Guarda cada uno en el estado y reinícialos cuando el efecto inicia una petición nueva.
Haz clic en User 3: la API falsa rechaza, el mensaje de error reemplaza el nombre y la consola muestra el error. El bloque finally desactiva loading en ambos caminos, así que una petición fallida nunca deja el indicador encendido.
Cuando fetch habla con un servidor real, una respuesta 404 o 500 no rechaza la promesa. Revisa res.ok y lanza el error tú mismo:
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
async/await dentro de un efecto
La función que pasas a useEffect no puede ser async. React espera que devuelva nada o una función de limpieza, y una función async siempre devuelve una promesa.
// Wrong: the effect returns a promise
useEffect(async () => {
const data = await fetchUser(id);
setUser(data);
}, [id]);
// Right: define an async function inside and call it
useEffect(() => {
async function load() {
const data = await fetchUser(id);
setUser(data);
}
load();
}, [id]);
Condiciones de carrera
Cuando el id cambia rápido, hay dos peticiones en curso a la vez. Nada garantiza que respondan en orden. Si la más vieja es más lenta, llega al final y sobrescribe los datos del id que el usuario eligió más recientemente.
La solución es una bandera ignore. Cada ejecución del efecto tiene su propia bandera, y su limpieza la pone en true. Como React ejecuta la limpieza antes de la siguiente ejecución, una respuesta de una ejecución desactualizada ve ignore === true y se descarta. Este bloque renderiza el mismo perfil dos veces, sin y con la bandera:
Haz clic en el botón y espera. La respuesta rápida del usuario 2 llega primero, así que ambos perfiles registran y se quedan con Grace. Alrededor de un segundo y medio después del clic llega la respuesta lenta del usuario 1: el perfil sin la bandera cambia a Ada aunque el usuario 2 está seleccionado, mientras que el que tiene la bandera registra dropped stale Ada y conserva a Grace.
El mismo error aparece en cajas de búsqueda, pestañas y cualquier lista filtrada por el servidor. El ejemplo de carga de arriba omite la bandera para ser breve, pero cada petición en un efecto debería tenerla, o la cancelación de abajo.
Cancelar con AbortController
La bandera ignore descarta una respuesta desactualizada, pero la petición sigue hasta el final. Con el fetch real puedes cancelarla. Crea un AbortController en el efecto, pasa su signal a fetch y llama a abort() en la limpieza:
useEffect(() => {
const controller = new AbortController();
async function load() {
try {
const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
setUser(await res.json());
} catch (err) {
if (err.name === 'AbortError') return; // cancelled on purpose
setError(err);
}
}
load();
return () => controller.abort();
}, [id]);
Un fetch cancelado rechaza con un AbortError, así que el bloque catch lo omite en lugar de mostrarlo como un fallo. La cancelación también cubre el desmontaje: cuando se quita el componente, su limpieza cancela la petición.
Llevarlo a un hook personalizado
La carga, el error y la protección contra carreras son iguales en cada componente que obtiene datos. Ponlos en un hook personalizado y cada componente pide los datos en una línea.
fetchPosts está definida fuera del componente, así que su identidad nunca cambia y es segura en el array de dependencias. Definida dentro de App, sería una función nueva en cada renderizado, el efecto se ejecutaría después de cada renderizado y, como el efecto cambia el estado, nunca dejaría de pedir datos.
Cambia 500 por 2000 y haz clic en el botón dos veces seguidas: la consola muestra dos peticiones, y solo aparecen las publicaciones del tema en el que terminaste.
Obtener datos en un manejador de eventos
Un efecto es para datos que el componente necesita porque está en pantalla: una página de perfil carga su perfil. Cuando una petición ocurre porque el usuario hizo algo, como pulsar Search o Save, hazla en el manejador de eventos. Sabes exactamente qué la provocó, y nada se vuelve a ejecutar cuando cambia un estado que no tiene relación.
async function handleSubmit(e) {
e.preventDefault();
setStatus('saving');
const res = await fetch('/api/notes', { method: 'POST', body: JSON.stringify({ text }) });
setStatus(res.ok ? 'saved' : 'error');
}
Errores comunes
Dependencias faltantes. Un efecto que lee id pero tiene [] pide el primer usuario para siempre. Incluye cada valor que use la petición.
Obtener datos en el cuerpo del componente. Un fetch fuera de un efecto se ejecuta en cada renderizado, y si cambia el estado, inicia un bucle.
Confiar en que fetch rechace ante errores. Solo rechaza cuando falla la red. Revisa res.ok.
Olvidar reiniciar la carga. Cuando cambia el id, vuelve a poner loading en true, o los datos viejos se quedan en pantalla sin ninguna señal de que vienen datos nuevos.
Cuándo usar una librería o un framework
Obtener datos en un efecto está bien para unas pocas peticiones. No guarda caché: abre el mismo perfil dos veces y lo pide dos veces. No comparte datos entre componentes, no reintenta cuando falla ni vuelve a pedir los datos cuando la pestaña recupera el foco. Las librerías hacen esto por ti:
import { useQuery } from '@tanstack/react-query';
function Profile({ id }) {
const { data, error, isPending } = useQuery({
queryKey: ['user', id],
queryFn: () => fetch(`/api/users/${id}`).then((res) => res.json()),
});
if (isPending) return <p>Loading...</p>;
if (error) return <p>{error.message}</p>;
return <p>{data.name}</p>;
}
TanStack Query y SWR son las opciones habituales para datos del lado del cliente. Los frameworks van más allá y cargan los datos en el servidor antes de que la página llegue al navegador: Next.js con server components, React Router con loaders. Eso evita el parpadeo de carga y la cadena de peticiones que empieza cuando los datos de un padre tienen que llegar antes de que un hijo pueda siquiera empezar a pedir los suyos.
React 19: use() con Suspense
React 19 agrega use, que lee una promesa durante el renderizado. El componente se suspende hasta que la promesa se resuelve, y el límite <Suspense> más cercano muestra un fallback mientras tanto, así que el propio componente no tiene estado de carga:
import { use, Suspense } from 'react';
function Profile({ userPromise }) {
const user = use(userPromise);
return <p>{user.name}</p>;
}
<Suspense fallback={<p>Loading...</p>}>
<Profile userPromise={userPromise} />
</Suspense>
La promesa tiene que crearse fuera del componente (por un framework, una caché o un padre), no dentro del renderizado que la lee, o cada renderizado inicia una petición nueva. La página del hook use lo cubre con ejemplos ejecutables.
Preguntas frecuentes
¿Cómo obtengo datos cuando carga un componente de React?
Inicia la petición en un useEffect con los valores de los que depende en el array de dependencias ([] si no hay ninguno), y guarda el resultado en el estado con useState. Renderiza un mensaje de carga hasta que lleguen los datos.
¿Por qué el callback de useEffect no puede ser async?
Una función async siempre devuelve una promesa, y React espera que el efecto devuelva nada o una función de limpieza. Escribe una función async dentro del efecto y llámala enseguida.
¿Qué es una condición de carrera al obtener datos en React?
Cuando la entrada cambia rápido, hay dos peticiones en curso y la más vieja puede responder al final, sobrescribiendo los datos más nuevos. Define una bandera ignore en la limpieza del efecto y omite setState cuando esté activa, o cancela la petición con un AbortController.
¿Debo usar useEffect o una librería para obtener datos?
useEffect funciona para apps pequeñas y vale la pena entenderlo. Para caché, deduplicación, reintentos y recargas, una librería como TanStack Query o SWR, o la carga de datos integrada en un framework como Next.js, te ahorra escribir esa lógica tú mismo.
¿Cómo muestro un indicador de carga mientras cargan los datos?
Guarda un booleano loading (o una cadena status) en el estado, actívalo antes de que empiece la petición y desactívalo cuando termine, tanto en el camino de éxito como en el de error. Renderiza el indicador mientras sea true.