
En resumen
- Un efecto corre después de pintar y solo en el navegador: lo que dependa de él no está en el HTML inicial.
- Toda petición dentro de un efecto necesita cancelación.
- Si un valor se puede calcular durante el render, no es estado ni necesita efecto.
- Título, descripción y canónica no van en useEffect: en React 19 se declaran en el componente.
- Cuando varios useState cambian juntos, es momento de useReducer.
Los errores en useEffect de React son la causa más frecuente de bugs difíciles de reproducir: datos viejos en pantalla, renders en cadena, ciclos infinitos. En una aplicación interna son una molestia. En un sitio que vive del tráfico orgánico, cuestan posiciones.
Trabajo en migraciones enterprise a React manteniendo el SEO. En ese contexto, useEffect deja de ser un tema de estilo y se vuelve una pregunta concreta: ¿qué parte del contenido existe en el HTML antes de que corra JavaScript?
Este artículo revisa los cuatro errores más comunes, cómo diagnosticarlos y cuándo el problema real es cómo modelas el estado. Los ejemplos están en TypeScript y son ilustrativos.
Por qué useEffect pesa más en una migración con SEO
Un sitio en WordPress o PHP entrega el HTML completo desde el servidor. El título, el encabezado y el contenido ya están ahí cuando llega la respuesta.
Al migrar a React, esa garantía desaparece si no la cuidas. Un efecto tiene dos propiedades que la rompen:
- Corre después del pintado. React primero muestra el componente y después ejecuta el efecto. Lo que el efecto cambie llega en un segundo render.
- No corre en el servidor. Durante el render en servidor los efectos no se ejecutan. Todo lo que dependa de ellos queda fuera del HTML inicial.
La consecuencia es directa. Si el contenido principal o los metadatos se resuelven dentro de un efecto, el HTML que recibe un rastreador sale incompleto, aunque en tu navegador todo se vea bien.
Por eso la primera regla de una migración con SEO es de arquitectura: los datos que definen la página se resuelven en el servidor. Los efectos quedan para lo que sí es un efecto: sincronizar con algo externo a React.
Los 4 errores más comunes en useEffect
1. Pedir datos sin cancelar la petición anterior
El usuario escribe en un buscador. Cada tecla dispara una petición. Las respuestas no llegan en orden, y la de una búsqueda vieja puede pisar a la más reciente.
Es una condición de carrera, y en desarrollo casi nunca aparece porque la red local es rápida. La solución es que cada efecto cancele su propia petición al limpiarse:
useEffect(() => {
const ctrl = new AbortController();
// Sin cancelación, una respuesta vieja puede pisar a la nueva.
fetch(`/api/productos?q=${encodeURIComponent(q)}`, { signal: ctrl.signal })
.then((r) => r.json())
.then(setProductos)
.catch((e) => {
// Abortar es parte del flujo normal, no un error que mostrar.
if (e.name !== "AbortError") setError(e);
});
// Se ejecuta al cambiar q y al desmontar: nada queda colgado.
return () => ctrl.abort();
}, [q]);
Si la petición define el contenido principal de la página, el arreglo de fondo es otro: sácala del efecto y resuélvela en el servidor o con una biblioteca de datos que maneje caché y cancelación.
2. Guardar estado derivado con un efecto
Es el error más extendido. Un valor que se puede calcular a partir de props o de otro estado se guarda en su propio useState, y un efecto lo «sincroniza».
El resultado es un render extra en cada cambio y una ventana en la que la pantalla muestra dos valores que no coinciden.
// Mal: estado duplicado y un render extra por cada cambio en items.
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((s, i) => s + i.precio * i.cantidad, 0));
}, [items]);
// Bien: se calcula durante el render. No hay nada que sincronizar.
const total = useMemo(
() => items.reduce((s, i) => s + i.precio * i.cantidad, 0),
[items],
);
La pregunta que lo detecta es simple: ¿puedo calcular esto con lo que ya tengo? Si la respuesta es sí, no es estado. Y useMemo solo hace falta cuando el cálculo es caro; para una suma corta, una constante basta.
3. Dependencias que mienten
El arreglo de dependencias es un contrato: le dice a React de qué valores depende el efecto. Hay dos formas de romperlo.
La primera es omitir una dependencia para «que no se dispare tanto». El efecto queda con un valor viejo capturado y el bug aparece semanas después, lejos de su causa.
La segunda es depender de un objeto o una función que se crea en cada render. Para React es un valor nuevo cada vez, así que el efecto corre siempre. Si además actualiza estado, tienes un ciclo infinito.
- Depende de valores primitivos. En lugar de
[filtros], usa[filtros.categoria, filtros.pagina]. - Mueve dentro del efecto lo que solo el efecto usa. Una función declarada adentro no necesita estar en las dependencias.
- Usa la forma funcional de
setState.setN((n) => n + 1)no necesita leerndesde afuera.
4. Resolver contenido y metadatos críticos en un efecto
Este es el error que más cuesta en una migración. Se ve inofensivo: cambiar document.title o insertar la descripción dentro de un useEffect.
Como los efectos no corren en el servidor, el HTML inicial sale con el título genérico de la aplicación. Todas las páginas comparten el mismo título hasta que JavaScript corre.
React 19 lo resuelve sin bibliotecas: las etiquetas <title>, <meta> y <link> se declaran en el componente y React las coloca en el <head>, también durante el render en servidor.
// Mal: el HTML inicial sale sin título ni descripción propios.
useEffect(() => {
document.title = producto.nombre;
}, [producto]);
// Bien (React 19): los metadatos viajan en el HTML del servidor.
export function FichaProducto({ producto }: { producto: Producto }) {
return (
<article>
<title>{`${producto.nombre} | Tienda`}</title>
<meta name="description" content={producto.resumen} />
<link rel="canonical" href={producto.url} />
<h1>{producto.nombre}</h1>
</article>
);
}
La misma regla aplica al contenido. El encabezado principal, el texto y los enlaces internos deben salir en el primer HTML. Un efecto puede enriquecer la página; no debe construirla.
Cómo diagnosticar problemas en useEffect
La mayoría de estos errores se detecta antes de producción si el equipo usa cuatro herramientas que ya tiene a la mano.
- El modo estricto. En desarrollo, React monta, limpia y vuelve a montar cada componente. Si un efecto se rompe al ejecutarse dos veces, le falta limpieza. No apagues el modo estricto para «arreglarlo».
- La regla
exhaustive-deps. El plugin de ESLint para hooks señala cada dependencia omitida. Trátala como error, no como advertencia. - El Profiler de React DevTools. Muestra qué componentes se renderizan y por qué. Dos renders seguidos por cada interacción suelen delatar estado derivado.
- El HTML sin JavaScript. Es la prueba que más importa para SEO, y la más ignorada.
Esa última prueba se automatiza con tres líneas. Lo que no aparece en esta respuesta no existe para un rastreador que no ejecuta JavaScript:
# Descarga el HTML tal como lo entrega el servidor, sin ejecutar JavaScript.
curl -s https://tu-sitio.example/producto/ejemplo/ > inicial.html
# Cada página debe traer su propio título, un solo h1 y su canónica.
grep -o "<title>[^<]*</title>" inicial.html
grep -c "<h1" inicial.html
grep -o '<link rel="canonical"[^>]*>' inicial.html
En una migración, esta revisión corre contra la lista completa de URLs, antes y después del cambio. Si una página pierde su título o su encabezado en el HTML inicial, se detiene el despliegue.
Prácticas recomendadas para useState y useReducer
Muchos efectos problemáticos son síntoma de otro problema: el estado está mal modelado. Cuando un efecto existe solo para mantener coordinados varios useState, el efecto no es la enfermedad.
El caso típico es una carga de datos con tres estados separados: cargando, error y datos. Nada impide que cargando y error sean verdaderos al mismo tiempo. Ese estado imposible tarde o temprano se pinta.
Un reducer con una unión discriminada elimina esas combinaciones desde el tipo:
type Estado =
| { fase: "inactivo" }
| { fase: "cargando" }
| { fase: "listo"; datos: Producto[] }
| { fase: "error"; mensaje: string };
type Accion =
| { tipo: "pedir" }
| { tipo: "ok"; datos: Producto[] }
| { tipo: "fallo"; mensaje: string };
// Todas las transiciones viven aquí: se prueban sin montar un componente.
function reducer(_: Estado, a: Accion): Estado {
switch (a.tipo) {
case "pedir": return { fase: "cargando" };
case "ok": return { fase: "listo", datos: a.datos };
case "fallo": return { fase: "error", mensaje: a.mensaje };
}
}
Con este modelo, el compilador no deja leer datos si la fase no es listo. Una clase entera de bugs deja de ser posible.
La regla práctica para elegir:
useStatepara valores independientes: un campo de texto, un interruptor, una pestaña activa.useReducercuando varios valores cambian juntos o cuando el siguiente estado depende de la acción y del estado anterior.- Ninguno de los dos para datos del servidor. Eso es caché, y merece una herramienta pensada para caché.
- La URL para lo que debe sobrevivir a recargar o compartir: filtros, página, orden. Además, es lo que un buscador puede enlazar.
Hay un punto en el que ni useReducer alcanza: cuando el mismo estado se necesita en ramas lejanas del árbol. Ahí toca subirlo a un contexto o a un almacén externo. Pero esa decisión se toma al final, no al principio.
Lo que NO debes hacer
- Silenciar la regla de dependencias. Un comentario para desactivar
exhaustive-depsno arregla el efecto; esconde el bug hasta que alguien cambie el código de alrededor. - Usar
useEffectcomo «al montar» para todo. Pedir datos, calcular valores y fijar metadatos al montar el componente es replicar el modelo de clases. Casi siempre hay una opción mejor: servidor, render o manejador de eventos. - Validar la migración solo en el navegador. Que la página se vea bien con JavaScript no dice nada del HTML inicial. Sin la prueba del HTML crudo, el problema lo descubres en el reporte de tráfico.
- Apagar el modo estricto. El doble montaje en desarrollo es una prueba gratuita. Si molesta, es porque encontró algo.
El siguiente paso
Corregir los errores en useEffect no es cuestión de memorizar reglas. Es decidir dónde vive cada cosa: los datos de la página en el servidor, los cálculos en el render, las transiciones en un reducer y los efectos solo para sincronizar con el exterior.
Si tu equipo enfrenta una migración a React y no puede darse el lujo de perder posicionamiento, o necesita auditar el rendimiento de un frontend que ya está en producción, en Conqr Digital Agency diseñamos arquitecturas que escalan. Revisemos tu caso.
Si quieres conocer cómo trabajo proyectos de este tipo, revisa el servicio de desarrollo web a medida.
Preguntas frecuentes
- ¿Por qué mi useEffect se ejecuta dos veces?
- En desarrollo, el modo estricto de React monta el componente, ejecuta el efecto, lo limpia y lo vuelve a ejecutar. Lo hace a propósito, para revelar efectos sin limpieza. En producción corre una sola vez.
- ¿useEffect afecta el SEO?
- De forma indirecta, sí. Los efectos solo corren en el navegador, después del primer pintado. Si el contenido principal, el título o la descripción dependen de un efecto, no están en el HTML que entrega el servidor.
- ¿Cuándo usar useReducer en lugar de useState?
- Cuando varios valores cambian juntos, cuando el siguiente estado depende del anterior y de la acción, o cuando hay combinaciones de estado que nunca deberían existir. Un reducer concentra las transiciones en un solo lugar.



