¿Alguna vez has construido un tooltip que salta desde la esquina superior izquierda hasta su posición correcta? ¿O un modal que parpadea con el tamaño equivocado antes de asentarse en su lugar? Ese glitch de una fracción de segundo es un parpadeo de diseño (layout flicker). Ocurre cuando React lee el DOM, calcula una corrección y actualiza el estado, pero el navegador ya ha comenzado a proyectar los píxeles en la pantalla. La receta habitual es cambiar useEffect por useLayoutEffect. El cambio funciona, pero solo si entiendes exactamente cuándo se dispara cada hook dentro del pipeline del navegador.

El Pipeline del Navegador: Render, Commit, Paint

React actualiza un componente en tres etapas distintas. En la fase de render, React construye —o reconstruye— el Virtual DOM y calcula el diff. Aún no hay cambios reales en los píxeles; esto es pura computación que ocurre en memoria. A continuación viene la fase de commit, donde React aplica esos cambios a los nodos reales del DOM. Los estilos se actualizan, los nodos se insertan o eliminan, y el texto cambia.

Luego el navegador toma el control. En la fase de paint, el motor de renderizado del navegador calcula la geometría del layout y dibuja los píxeles en la pantalla. Esta secuencia es rígida. El navegador debe completar el layout antes de poder pintar, y debe terminar de pintar antes de que el usuario vea algo nuevo. La brecha entre el commit y el paint se mide en milisegundos, pero es real, y es la ventana donde useEffect y useLayoutEffect divergen.

Por qué useEffect causa el parpadeo

useEffect se ejecuta de forma asíncrona, programada para activarse después de que el navegador ya haya pintado la pantalla. El DOM se actualiza, los píxeles se dibujan y luego React interviene para ejecutar tu efecto.

Imagina que renderizas un menú desplegable debajo de un botón. Dentro de useEffect, llamas a buttonRef.current.getBoundingClientRect(), calculas las coordenadas correctas de top y left, y las guardas en el estado. Debido a que useEffect se ejecuta después del paint, el navegador ya ha dibujado el menú desplegable en su posición por defecto, quizás en top: 0, left: 0. Solo después de ese paint tu efecto actualiza el estado. React aplica las coordenadas corregidas mediante un commit, y el navegador vuelve a pintar. El usuario ve dos fotogramas: la posición incorrecta y luego la correcta. Ese salto visual es el parpadeo que todos intentan evitar.

Para la obtención de datos, llamadas a APIs, seguimiento de analíticas o la configuración de event listeners, este retraso no importa. Al usuario no le importa si un beacon de analíticas se dispara unos milisegundos después del paint. De hecho, posponer el trabajo no visual hasta después del paint mantiene la renderización inicial con una respuesta rápida. Pero para correcciones que dependen del layout, useEffect llega simplemente demasiado tarde.

Cómo useLayoutEffect bloquea el Paint

useLayoutEffect se ejecuta de forma síncrona, inmediatamente después de que React muta el DOM pero antes de que el navegador tenga la oportunidad de calcular el layout o pintar los píxeles. Bloquea completamente el pipeline de pintado.

Si realizas la misma medición del menú desplegable dentro de useLayoutEffect, la secuencia cambia. React aplica la actualización inicial del DOM, ejecuta tu efecto de layout, y la actualización de tu estado dispara un re-renderizado síncrono. React aplica las coordenadas corregidas y solo entonces el navegador pinta. El usuario ve un solo fotograma, que ya es correcto.

Ese comportamiento de bloqueo es tanto una ventaja como un riesgo. Debido a que useLayoutEffect impide que el navegador pinte hasta que termine, cualquier computación pesada dentro de él congela la interfaz de usuario. Incluso unas pocas docenas de milisegundos de pintado bloqueado se sienten como jank (falta de fluidez) para el usuario. Es por esto que la documentación de React te indica explícitamente que comiences con useEffect y solo pases a useLayoutEffect cuando realmente observes un parpadeo que no puedas tolerar.

Cuándo usar cada Hook

La mayor parte de tu lógica debe pertenecer a useEffect. Úsalo para:

  • Obtener datos de una API
  • Configurar suscripciones o event listeners
  • Enviar eventos de analíticas
  • Cualquier efecto secundario que no lea ni mute el layout de inmediato

Reserva useLayoutEffect para operaciones que deben leer el DOM y escribir de nuevo antes de que el usuario vea el fotograma:

  • Medir las dimensiones de un elemento, como el ancho, el alto o la posición de desplazamiento (scroll)
  • Calcular coordenadas para tooltips, popovers o menús contextuales
  • Prevenir cambios de diseño visibles cuando la posición visual depende de la geometría renderizada

Si no estás seguro de cuál elegir, opta por defecto por useEffect. Pásate a useLayoutEffect solo cuando notes inestabilidad visual. Esta regla por sí sola mantendrá la gran mayoría de las aplicaciones de React funcionando sin problemas.

El problema del Server-Side Rendering

Si usas Next.js, Remix o cualquier framework que renderice React en el servidor, te encontrarás con una advertencia con useLayoutEffect. Debido a que el servidor no tiene DOM, el hook no tiene nada que medir. React te advierte que esperaba un entorno de navegador y no lo encontró. Durante la hidratación, este desajuste también puede causar errores sutiles porque el marcado renderizado en el servidor y el primer renderizado previsto del cliente pueden diferir.

La solución estándar es un hook isomórfico que selecciona el efecto adecuado según el entorno:

const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

Usa este wrapper en cualquier componente que deba medir nodos del DOM pero que pueda ejecutarse durante el renderizado en el servidor. Silencia la advertencia y mantiene la consistencia de la salida de tu servidor.

Rendimiento y mejores prácticas

Dado que useLayoutEffect bloquea el pintado, mantén el cuerpo del hook lo más ligero posible. Lee el valor del layout, calcula la corrección y vuelve a escribirlo. No realices peticiones de datos, analices objetos grandes ni ejecutes algoritmos costosos dentro de él. El código pesado aquí bloqueará el hilo principal y hará que tu interfaz se sienta congelada.

Cuando midas elementos, utiliza React refs en lugar de document.getElementById. Los refs están vinculados a la instancia de tu componente, sobreviven a los re-renders sin trucos de consulta y funcionan de manera confiable con portales o renderizado condicional. Las búsquedas de ID globales rompen la encapsulación del componente y pueden devolver null justo en el momento en que los necesitas.

useEffect es la opción predeterminada correcta para casi cualquier efecto secundario. Permite que el navegador pinte sin interrupciones y gestiona datos, eventos y sincronización externa de forma limpia. useLayoutEffect es una herramienta especializada para un problema específico: leer el layout y escribir de nuevo antes del pintado. Domina la diferencia de tiempos entre ambos y dejarás de perseguir parpadeos para empezar a prevenirlos.

La conclusión clave: Empieza con useEffect para todo. En el momento en que veas un tooltip o un modal parpadear en el lugar equivocado antes de corregirse, esa será tu señal. Cambia a useLayoutEffect, mide el DOM, ajusta tu layout y deja que el navegador pinte una sola vez, correctamente.