useLayoutEffect
useLayoutEffect
es una versión de useEffect
que se acciona antes que el navegador vuelva a pintar la pantalla.
useLayoutEffect(setup, dependencies?)
Referencia
useLayoutEffect(setup, dependencies?)
Llama a useLayoutEffect
para ejecutar las medidas del layout antes que el navegador vuelva a pintar la pantalla:
import { useState, useRef, useLayoutEffect } from 'react';
function Tooltip() {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height);
}, []);
// ...
Parámetros
-
setup
: La función con la lógica de tu Efecto. Tu función setup puede devolver opcionalmente una función de limpieza. Antes que tu componente sea agregado al DOM, React va a ejecutar tu función setup. Después de cada renderizado con dependencias modificadas, React primero va a ejecutar la función de limpieza (si tú lo provees) con los valores anteriores, y luego ejecuta tu función setup con los nuevos valores. Antes que tu componente sea eliminado del DOM, React va a ejecutar tu función de limpieza. -
opcional
dependencies
: La lista de todos los valores reactivos referenciados dentro del código desetup
. Los valores reactivos incluyen props, estados, y todas las variables y funciones declaradas directamente dentro del cuerpo de tu componente. Si tu linter está configurado para React, va a verificar que cada valor reactivo este correctamente especificado como una dependencia. La lista de dependencias tiene que tener un número constante de elementos y ser escritos en linea como[dep1, dep2, dep3]
. React va a comparar cada dependencia con su valor anterior usando el algoritmo de comparaciónObject.is
. Si no especificas del todo las dependencias, tu Efecto se volverá a ejecutar después de cada renderizado del componente.
Devuelve
useLayoutEffect
devuelve undefined
.
Advertencias
-
useLayoutEffect
es un Hook, así que solo puedes llamarlo en el nivel mas alto de tu componente o en tus propios Hooks. No puedes llamarlo dentro de bucles o condicionales. Si lo necesitas, extrae un nuevo componente y mueve el estado a él. -
Cuando el Modo Estricto está activado, React va a ejecutar solo en desarrollo un ciclo adicional de setup+limpieza antes del primer setup real. Esta es una prueba de estrés que asegura que tu lógica de limpieza sea un «espejo» de tu lógica del setup y se detenga o se deshaga lo que sea que tu setup esté haciendo. Si esto causa un problema necesitas implementar la función de limpieza.
-
Si algunas de tus dependencias son objetos o funciones definidas dentro del componente, hay un riesgo de que ellas causen el efecto de volver a ejecutarse más de lo necesario. Para arreglar esto, elimina dependencias de objetos y funciones innecesarias. También puedes extraer actualizaciones de estados y lógica que no es reactiva fuera de tu Efecto.
-
Los Efectos solo se ejecuta en el lado del cliente. No se ejecutan durante el renderizado del lado del servidor
-
El código dentro de
useLayoutEffect
y todas las actualizaciones de estado programadas desde él bloquean el navegador de volver a pintar en la pantalla. Cuando es usado excesivamente, puede hacer tu aplicación muy lenta. Cuando sea posible se prefiere usaruseEffect
. -
If you trigger a state update inside
useLayoutEffect
, React will execute all remaining Effects immediately includinguseEffect
.
Uso
Medir el layout antes que el navegador vuelva a pintar la pantalla
La mayoría de los componentes no necesitan conocer sus posiciones y tamaños en la pantalla para decidir qué renderizar. Ellos solo devuelven algo de JSX con CSS. Luego, el navegador calcula sus layout (posición y tamaño) y vuelve a pintar la pantalla.
A veces, eso no es suficiente. Imagina un tooltip que aparece junto a algún elemento cuando pasas con el ratón por encima de él. Si hay suficiente espacio, el tooltip debe aparecer arriba del elemento, pero si no tiene suficiente espacio para encajar, debe aparecer debajo. Esto significa que para renderizar el tooltip en la posición final correcta, necesitas saber su altura (quiere decir, si cabe en la parte superior).
Para hacer esto, necesitas renderizar en dos pasadas:
- Renderiza el tooltip en cualquier lugar (incluso con una posición incorrecta).
- Mide su altura y decide dónde colocar el tooltip.
- Renderiza el tooltip de nuevo en la posición correcta.
Todo esto necesita pasar antes que el navegador vuelva a pintar la pantalla. No quieres que el usuario vea el tooltip moviéndose. Llama a useLayoutEffect
para llevar a cabo las medidas del layout antes que el navegador vuelva a pintar la pantalla.
function Tooltip() {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0); // Aún no sabes la altura real
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height); // Vuelve a renderizar ahora que sabes la altura real
}, []);
// ...debajo, usa tooltipHeight en la lógica del renderizado...
}
Así es como funciona paso por paso:
Tooltip
se renderiza inicialmente contooltipHeight = 0
(el tooltip puede estar posicionado incorrectamente).- React lo coloca en el DOM y ejecuta el código en
useLayoutEffect
. - Tu
useLayoutEffect
mide la altura del contenido del tooltip y dispara inmediatamente un renderizado de nuevo. Tooltip
se vuelve a renderizar con eltooltipHeight
real (el tooltip está posicionado correctamente).- React lo actualiza en el DOM y el navegador finalmente muestra el tooltip.
Pasa el ratón por encima de los botones debajo y mira como el tooltip ajusta su posición dependiendo de si encaja.
import { useRef, useLayoutEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import TooltipContainer from './TooltipContainer.js'; export default function Tooltip({ children, targetRect }) { const ref = useRef(null); const [tooltipHeight, setTooltipHeight] = useState(0); useLayoutEffect(() => { const { height } = ref.current.getBoundingClientRect(); setTooltipHeight(height); console.log('Altura del tooltip medida: ' + height); }, []); let tooltipX = 0; let tooltipY = 0; if (targetRect !== null) { tooltipX = targetRect.left; tooltipY = targetRect.top - tooltipHeight; if (tooltipY < 0) { // No encaja arriba, entonces colócalo debajo tooltipY = targetRect.bottom; } } return createPortal( <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}> {children} </TooltipContainer>, document.body ); }
Ten en cuenta que aunque el componente Tooltip
tiene que renderizar en dos pasos (primero con tooltipHeight
inicializado en 0
y luego con la medición real de la altura), tú solo ves el resultado final. Es por esto que necesitas useLayoutEffect
en vez de useEffect
para este ejemplo. Veamos las diferencias en detalle debajo.
Ejemplo 1 de 2: useLayoutEffect
bloquea el navegador para que no vuelva a pintarse
React garantiza que el código dentro de useLayoutEffect
y cada actualización de estado programada dentro de él va a ser procesada antes que el navegador vuelva a pintar la pantalla. Esto te permite renderizar el tooltip, medirlo, y volver a renderizar el tooltip sin que el usuario note el primer renderizado adicional. En otras palabras, useLayoutEffect
bloquea el navegador de pintarse.
import { useRef, useLayoutEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import TooltipContainer from './TooltipContainer.js'; export default function Tooltip({ children, targetRect }) { const ref = useRef(null); const [tooltipHeight, setTooltipHeight] = useState(0); useLayoutEffect(() => { const { height } = ref.current.getBoundingClientRect(); setTooltipHeight(height); }, []); let tooltipX = 0; let tooltipY = 0; if (targetRect !== null) { tooltipX = targetRect.left; tooltipY = targetRect.top - tooltipHeight; if (tooltipY < 0) { // No encaja arriba, entonces colócalo debajo tooltipY = targetRect.bottom; } } return createPortal( <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}> {children} </TooltipContainer>, document.body ); }
Solución de problemas
Estoy teniendo un error: «useLayoutEffect
no hace nada en el servidor»
El propósito de useLayoutEffect
es dejar a tu componente usar información del layout para renderizar:
- Renderizar el componente inicial.
- Medir el layout antes que el navegador vuelva a pintar en la pantalla.
- Renderizar el contenido usando la información del layout que has leído.
Cuando tú o tu framework usa renderizado del lado del servidor, tu aplicación de React renderiza a HTML en el servidor en el renderizado inicial. Esto te permite mostrar el HTML inicial antes que el código Javascript cargue.
El problema está que en el servidor no hay información de layout.
En el ejemplo del principio, useLayoutEffect
llama al componente Tooltip
, se posiciona a sí mismo correctamente (ya sea arriba o debajo del contenido) dependiendo de la altura del contenido. Si trataras de renderizar Tooltip
como parte del HTML inicial del servidor, esto sería imposible de determinar. ¡En el servidor no hay navegador ni layout! Incluso si lo renderizas en el servidor, su posición «saltará» en el cliente después de que el código JavaScript cargue y se ejecute.
Usualmente, los componentes que dependen de la información del layout no necesitan renderizarse en el servidor de todos modos. Por ejemplo, probablemente no tiene sentido mostrar un Tooltip
durante el renderizado inicial. Se desencadena por una interacción del cliente.
Sin embargo, si estás pasando por este problema, tienes algunas opciones:
-
Puedes reemplazar
useLayoutEffect
conuseEffect
. Esto le dice a React que está bien mostrar el resultado inicial del renderizado sin bloquear el pintado (porque el HTML original se convierte en visible antes que tu Efecto se ejecute). -
Puedes marcar tu componente como solo cliente. Esto le indica a React que debe reemplazar su contenido hasta la barrera de
<Suspense>
más cercana con un fallback de carga (por ejemplo, un spinner o un glimmer) durante el renderizado en el lado del servidor. -
Puedes mostrar diferentes componentes en el servidor y en el cliente. Una manera de hacer esto es mantener el estado booleano
isMounted
que está inicializado enfalse
, y cambiarlo atrue
dentro de la llamada de unuseEffect
. La lógica de renderizado puede ser entonces comoreturn isMounted ? <RealContent /> : <FallbackContent />
. En el servidor y durante la hidratación, el usuario va a verFallbackContent
que no debe llamaruseLayoutEffect
. Luego React va a reemplazarlo conRealContent
que se ejecuta solo en el lado del cliente y puede incluir llamadas auseLayoutEffect
. -
Si sincronizas tu componente con un almacén externo de datos y dependes de
useLayoutEffect
por diferentes razones que medir el layout, considera en su lugar usaruseSyncExternalStore
que soporta renderizado del lado del servidor.