El «parpadeo» parece un solo bug, pero en realidad son dos, con causas completamente distintas. Si los distingues primero, ya tienes el 90% resuelto.
Step 1: ¿De qué tipo de parpadeo se trata?
- A. Un destello al cargar: en el momento en que aparece la página ves una pantalla en blanco (o equivocada) y luego cambia — por ejemplo, destella claro antes de que entre el modo oscuro, o muestra la vista de sesión cerrada por una fracción de segundo antes de tu pantalla real. → Normalmente es un hydration mismatch o un theme flash.
- B. Parpadeo que no para: la pantalla no deja de temblar o parece que se recarga sin fin, la pestaña se pone lenta, el ventilador se acelera. → Normalmente es un infinite re-render loop.
Para asegurarte, abre las DevTools del navegador con F12 → pestaña Console y lee el error en rojo:
Hydration failed/Text content did not match→ AMaximum update depth exceeded→ B
---
B. Parpadeo que no para: infinite re-render loop (lo más común)
Si la consola dice Maximum update depth exceeded, es eso. La causa es casi siempre la misma: un useEffect actualiza el state, y ese nuevo state vuelve a disparar el mismo useEffect, una y otra vez.
Pégale esto a tu IA (Cursor / v0 / Bolt / Lovable)
> La consola muestra Maximum update depth exceeded y mi pantalla parpadea sin parar. Parece que un useEffect llama a setState con un array de dependencias incorrecto, provocando un bucle infinito. Encuentra qué useEffect es el culpable y arréglalo: corrige el array de dependencias o usa la forma de actualización (setCount(c => c + 1)). Arregla la causa raíz, no te limites a ocultar el aviso.
Si sabes leer el código (dos soluciones oficiales de React)
1) Actualiza a partir del valor anterior — quita la dependencia
// Problem: count changes every run, so the effect re-runs forever
useEffect(() => {
setCount(count + 1);
}, [count]);
// Fix: updater function + empty deps
useEffect(() => {
setCount(c => c + 1);
}, []);
2) No pases un objeto/función como dependencia — créalo dentro del effect
// Problem: a new options object every render → infinite loop
const options = { serverUrl, roomId };
useEffect(() => {
createConnection(options);
}, [options]);
// Fix: build it inside the effect
useEffect(() => {
const options = { serverUrl, roomId };
createConnection(options);
}, [roomId, serverUrl]);
Comprobación rápida: sospecha primero de cualquier useEffect(() => { ... }) sin array de dependencias: sin [] se ejecuta en cada render y entra en bucle con facilidad.
---
A. Un destello al cargar: hydration mismatch / theme flash
Con frameworks como Next.js, el servidor prerrenderiza el HTML y el navegador vuelve a renderizar en el primer pintado. Si esos dos no coinciden, la pantalla da un salto una vez. La consola muestra Text content did not match / Hydration failed.
Causas comunes (según la documentación de Next.js)
- Usar
window,localStorageoDate()directamente en tu código de renderizado - Ramificar la UI con comprobaciones como
typeof window !== 'undefined' - Leer el tema oscuro/claro solo en el navegador, de modo que el primer pintado sale con el tema equivocado
- Una extensión del navegador (traductor, extensión de modo oscuro) editando el HTML → pruébalo en una ventana de incógnito para confirmarlo
Fix 1: Aplaza las partes solo-navegador con useEffect
Renderiza lo mismo en el servidor y en el primer pintado del cliente, y luego cambia.
import { useState, useEffect } from 'react'
export default function App() {
const [isClient, setIsClient] = useState(false)
useEffect(() => { setIsClient(true) }, [])
return <h1>{isClient ? 'Browser-only content' : 'Prerendered'}</h1>
}
Fix 2: Desactiva el renderizado en servidor para un componente (mapas, gráficos, widgets solo-navegador)
import dynamic from 'next/dynamic'
const NoSSR = dynamic(() => import('../components/no-ssr'), { ssr: false })
Fix 3: Para valores que deben diferir (relojes, fechas), silencia solo ese elemento
<time dateTime="2016-10-25" suppressHydrationWarning />
Nota: es una vía de escape que funciona solo un nivel hacia abajo, y React no parcheará el texto que no coincide — no abuses de ella.
Si es un destello de modo oscuro (blanco → oscuro)
Con next-themes, añade suppressHydrationWarning a la etiqueta <html> de nivel superior, y renderiza la UI que depende del tema solo después de que el componente se haya montado.
// layout.jsx (app directory)
<html lang="en" suppressHydrationWarning>
// A component that renders differently by theme
const [mounted, setMounted] = useState(false)
useEffect(() => { setMounted(true) }, [])
if (!mounted) return null
next-themes inyecta un script que se ejecuta antes de que la página se renderice, así que por defecto no debería haber destello. Si aun así parpadea, lo más probable es que te falte una de las dos cosas de arriba (el suppressHydrationWarning, o renderizar después del montaje).
---
Un mensaje universal para pasarle a tu IA
> Mi pantalla [destella una vez al cargar / parpadea sin parar]. El error de la consola es [pega el mensaje exacto]. Encuentra el componente que lo provoca y arréglalo de la forma oficial. Arregla la causa raíz, no te limites a ocultar el aviso.
Cuando todavía no hay una solución confirmada (qué puedes hacer ya)
- Desactiva las extensiones del navegador (traductores, complementos de modo oscuro) y comprueba en una ventana de incógnito. Si se detiene, la extensión era la culpable.
- Copia el error completo de la consola y pégalo tal cual a tu IA — la primera línea suele apuntar al archivo y número de línea exactos.
