Inicio / Artículos

Qué le cuesta usar useEffect para hacer fetch y qué cambia con TanStack Query

Por qué la obtención de datos basada en efectos genera solicitudes duplicadas, código genérico y cierres obsoletos en React, y cómo las claves, staleTime e invalidation de TanStack Query solucionan estos problemas.

Editor: REACTAPP.TOP6 min de lectura1360 palabras

Fuentes: Stop Using useEffect for API Calls, Use React Query Instead

Obtener datos dentro de useEffect es el primer patrón que aprenden la mayoría de los desarrolladores de React, y funciona bien para una sola solicitud en una única página. A medida que la aplicación crece, este mismo patrón multiplica silenciosamente las solicitudes, repite código genérico en cada componente y genera errores de dependencia sutiles. Este artículo explica de dónde provienen esos costos y cómo TanStack Query (anteriormente React Query) aborda cada uno de ellos, para que puedas decidir cuándo hacer el cambio y qué configurar una vez lo hayas hecho.

El patrón con el que todos comienzan

La estructura habitual es un efecto con un array de dependencias vacío que llama a una función fetch cuando el componente se monta:

useEffect(() => {
  fetchData();
}, []);

Parece algo sencillo, y eso es parte del problema. El código no menciona nada sobre caché, deduplicación, intentos repetidos, cancelación ni qué debe ocurrir cuando el componente se monta nuevamente. Cada una de estas omisiones se convierte en un error o en una solicitud adicional cuando hay docenas de endpoints y más estados de carga y errores que monitorear.

Por qué la solicitud se envía dos veces y el verdadero problema detrás de ello

Una sorpresa común al principio es ver la misma solicitud dos veces en la pestaña de red al cargar la página. En el entorno de desarrollo, StrictMode de React monta, desmonta y vuelve a montar componentes intencionadamente para revelar efectos que no se limpian por sí solos. La solicitud duplicada es intencional en el desarrollo y no ocurre en la versión de producción.

La llamada duplicada es un síntoma de algo que realmente importa en la producción: una solicitud basada en efectos está vinculada al ciclo de vida del componente. Cada vez que se monta un componente, se genera una nueva solicitud, incluso cuando los datos ya están disponibles. Los componentes que se montan repetidamente, como los paneles de pestañas, los modales y las filas de listas, siguen solicitando datos al servidor, lo que aumenta la carga del mismo y hace que la interfaz parezca más lenta de lo necesario.

TanStack Query separa los datos del componente. Los resultados se almacenan en una caché compartida bajo una clave de consulta. Cuando otro componente, o el mismo después de un nuevo montaje, solicita una clave cuyos datos aún se consideran actualizados, la biblioteca devuelve el valor almacenado en caché sin necesidad de conectarse a la red. Varios componentes que solicitan la misma clave al mismo tiempo también comparten una sola solicitud en curso.

Estado de carga y errores gestionado manualmente

Con useEffect, cada solicitud necesita su propio trío de variables de estado para los datos, la bandera de carga y el error:

const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

Al repetir esto en cada llamada a la API, se genera mucho código casi idéntico, donde cada copia tiene sus propias posibilidades de cometer errores. El más común es olvidar restablecer loading en el caso de error, lo que deja un indicador de carga activo sin cesar. Otros errores incluyen no ignorar respuestas que llegan después de que el componente haya sido desmontado, y permitir que una respuesta anterior lenta sobrescriba a una más reciente cuando los datos cambian rápidamente.

useQuery devuelve directamente data, error, isLoading y isError (junto con campos de estado más detallados), y se encarga por usted de manejar las respuestas fuera de orden. El código genérico desaparece de los componentes, y los casos límite se resuelven una sola vez en una biblioteca bien probada en lugar de muchas veces en el código de la aplicación.

Arrays de dependencias versus claves de consulta

Los efectos dependen de configurar correctamente el array de dependencias. Si se omite un valor que el efecto necesita, se genera un cierre obsoleto que realiza búsquedas con parámetros desactualizados, un error que puede llevar horas resolver. Si se incluye un objeto o función que se recrea en cada renderizado, el efecto se ejecutará en cada uno de ellos, enviando una solicitud en cada ocasión.

TanStack Query reemplaza esta especulación por un contrato más claro: le proporcionas a cada consulta una clave y una función de obtención de datos. La clave debe incluir todas las variables de las que depende la solicitud, como por ejemplo el nombre del recurso junto con un ID de usuario o un filtro. Cuando la clave cambia, se ejecuta una nueva consulta; cuando permanece igual, se reutilizan los datos en caché. Las claves se comparan por su contenido y no por referencia, por lo que crear un nuevo array en cada renderizado no provoca solicitudes adicionales. La recarga de datos se controla mediante políticas explícitas, como el montaje de un componente, la recuperación del foco en la ventana o la reconexión de la red, combinadas con el staleTime que configures.

Aún así debes pensar en las dependencias, pero las expresas una sola vez en una clave en lugar de mantener un array que debe estar sincronizado con el cuerpo del efecto.

Caché, staleTime e invalidación

Tres características explican la mayor parte de las diferencias en el trabajo diario.

El caché

Después de que se obtienen los datos de una clave, estos permanecen en el caché. Cuando el usuario se aleja y vuelve, el resultado almacenado en caché se muestra de inmediato; si los datos están desactualizados, la biblioteca los vuelve a obtener en segundo plano y actualiza la pantalla cuando llega la nueva respuesta.

staleTime

staleTime controla cuánto tiempo los datos se consideran actualizados. Mientras estén actualizados, las nuevas cargas y los nuevos suscriptores no generan ninguna solicitud. El valor por defecto es cero, lo que significa que los datos se consideran desactualizados de inmediato; los datos en caché siguen mostrándose al instante, pero se vuelve a obtener información en segundo plano la próxima vez que se cargue la página o se enfocar la ventana. Establecer un valor razonable para staleTime en el caso de datos que no cambian cada segundo suele ser la forma más efectiva de reducir el tráfico del servidor.

invalidateQueries

Después de una mutación, como actualizar un perfil o crear una publicación, se llama a queryClient.invalidateQueries con la clave correspondiente. Las consultas que coinciden se marcan como obsoletas y aquellas que se están utilizando en ese momento se vuelven a cargar automáticamente. No hay necesidad de realizar una carga manual después de cada mutación, ni de copiar manualmente las respuestas del servidor al estado local.

Juntos, estos tres elementos abordan la mayor parte de los problemas que genera la carga basada en efectos. Para conocer más sobre cómo organizar claves, opciones de consulta y mutaciones a gran escala, consulte nuestra guía sobre estructurar una capa de datos TanStack Query.

Un escenario típico de migración

Considere un panel de control SaaS de Next.js con varias pestañas, cada una de las cuales carga sus propios datos mediante useEffect. Dado que al cambiar de pestaña se vuelve a cargar los paneles, cada cambio de pestaña envía una nueva oleada de solicitudes. El backend recibe mucho más tráfico del necesario y los usuarios notan una carga lenta.

Una migración suele seguir tres pasos:

  1. Sustituir cada efecto por useQuery y una clave que capture todos los parámetros de la solicitud.
  2. Asignar a cada consulta un staleTime que refleje con qué frecuencia realmente cambian sus datos.
  3. Después de cada mutación, invalidar las claves cuyos datos se ven afectados.

El resultado esperado es un panel de control más fluido, menos llamadas al backend y una mejor experiencia para los usuarios, además de menos código en cada componente.

Dónde sigue siendo adecuado useEffect

Esto no es un caso en el que sea necesario eliminar por completo useEffect. Sigue siendo la herramienta adecuada para sincronizarse con elementos externos a React que no son datos de tipo solicitud y respuesta: suscripciones, oyentes de eventos del DOM, temporizadores y widgets de terceros. Sin embargo, para las llamadas a API en una aplicación en crecimiento, una biblioteca dedicada al estado del servidor es la opción más adecuada, ya que reduce la carga del backend, elimina el código genérico y ofrece funcionalidades de caché e invalidación integradas.

También hay casos en los que una biblioteca es más de lo necesario, como en una aplicación pequeña con una o dos solicitudes. Para esos casos, un pequeño gancho personalizado con capacidad de cancelación adecuada puede ser suficiente; nuestro artículo sobre un gancho useFetch mínimo y sus limitaciones aborda ese equilibrio. Si no está seguro, migre primero una página y compare las cantidades de solicitudes en la pestaña de red.

Puntos clave

  • La doble solicitud en el desarrollo se debe a StrictMode; el problema en producción es que las solicitudes basadas en efectos se repiten con cada montaje.
  • Una clave de consulta reemplaza al array de dependencias: incluya todos los elementos en los que depende la solicitud, y el cacheo y la nueva carga se gestionarán a partir de ello.
  • El valor por defecto de staleTime es cero, así que úselo intencionadamente si desea evitar nuevas cargas en segundo plano al volver a montar.
  • Invalida las claves relacionadas después de las mutaciones en lugar de volver a obtener o sincronizar manualmente el estado.
  • Utiliza useEffect para sincronizarte con sistemas externos, no para cargar datos del servidor.
  • Lecturas relacionadas