Saltar al contenido

Blog técnico | Obteniendo el tamaño y la posición de un elemento en reacción

Introducción

Obtener el tamaño y la posición de los elementos en React no es una gran historia. Cada opción que investigué tiene al menos un «gotcha». Compartiré las mejores opciones que se me ocurrieron y explicaré los pros y los contras de cada una. Primero veamos la forma básica de obtener el tamaño y la posición de un elemento en React.

Obtener el tamaño y la posición

Puedes usar Element.getClientRects() y Element.getBoundingClientRect() para obtener el tamaño y la posición de un elemento. En React, primero necesitarás obtener una referencia a ese elemento. Aquí hay un ejemplo de cómo podrías hacer eso.

Blog técnico | Obteniendo el tamaño y la posición de un elemento en reacción
Blog técnico | Obteniendo el tamaño y la posición de un elemento en reacción
funciónRectánguloComponente(){retorno(<divref={el= >{//el puede ser nulo - ver https://reactjs.org/docs/refs-and-the-dom.html#caveats-with-callback-refsif(!el)return;console.log(el.getBoundingClientRect().width);// imprime 200px}}}style={{{display: "inline-block",width: "200px",height: "100px",background:blue}}/>);}

Esto imprimirá el ancho del elemento a la consola. Esto es lo que esperamos porque fijamos el ancho a 200px en el atributo de estilo.

El problema

Este enfoque básico fallará si el tamaño o la posición del elemento es dinámico, como en los siguientes escenarios.

  • El elemento contiene imágenes y otros recursos que se cargan asincrónicamente
  • Animaciones
  • Contenido dinámico
  • Redimensionamiento de la ventana

Todo esto es bastante obvio, ¿verdad? Aquí hay un escenario más escurridizo.

functionComponentWithTextChild(){return(<divref={el= >{if(!el)return;console.log(el.getBoundingClientRect().width);setTimeout(()= >{// suele imprimir un valor mayor que el primero console.logconsole.log("later",el.getBoundingClientRect(). width);});setTimeout(()= >{/// suele imprimir un valor mayor que el segundo console.logconsole.log("way later",el.getBoundingClientRect(). width);},1000);}}style={{{{{visualizar: "bloque en línea"};};div,} Checkitout,aquí es un texto en cada elemento de construcción</div>};}

Este ejemplo muestra un simple div con un solo nodo de texto como su único hijo. Registra el ancho de ese elemento inmediatamente, luego otra vez en el siguiente ciclo del bucle de eventos y una tercera vez un segundo después. Como sólo tenemos contenido estático, se podría esperar que el ancho fuera el mismo en las tres ocasiones, pero no lo es. Cuando ejecuté este ejemplo en mi ordenador, el primer ancho era de 304.21875, la segunda vez de 353.125 y la tercera de 358.078.

Curiosamente, este problema no ocurre cuando realizamos las mismas manipulaciones de DOM con el JS de vainilla.

constdiv=documento.createElement($0027div$0027)div.style.display=$0027inline-block$0027;constp=documento.createElement($0027p$0027);p.innerText=$0027Hola mundo esto es algo de texto$0027;div.appendChild(p);document.body.appendChild(div);console.log($0027width after appending$0027,div. getBoundingClientRect().width);setTimeout(()= >console.log($0027ancho después de un tick$0027,div.getBoundingClientRect().width));setTimeout((()= >console.log($0027ancho después de un 100ms$0027,div.getBoundingClientRect().width),100);

Si pegas esto en una consola, verás que el valor de ancho inicial es correcto. Por lo tanto, nuestro problema es específico de Reaccionar.

Solución #1: Encuesta

Una solución natural para esto es simplemente hacer una encuesta para los cambios de tamaño y posición.

funciónComponente que EncuestaPorAncho(){retorno(<divref={el= );{if(!el)retorno;console.log("ancho inicial",el.getBoundingClientRect(). width);letprevValue=JSON.stringify(el.getBoundingClientRect());conststart=Fecha.now();consthandle=setInterval((()=>{letnextValue=JSON.stringify(el. getBoundingClientRect());if(nextValue===prevValue){clearInterval(handle);console.log(`ancho dejó de cambiar en ${Fecha.ahora()-inicio}ms. ancho final:`,el.getBoundingClientRect(). width);}else{prevValue=nextValue;}},100);}}style={{{display: "inline-block"};};div>Checkitout,hereisometextinachildelement</div></div>);}

Aquí podemos ver los valores que cambian con el tiempo y sobre el tiempo que se tarda en obtener un valor final. En mi entorno, era alrededor de 150ms en una actualización de página completa, aunque lo estoy renderizando en Storybook, lo que podría estar añadiendo algunos gastos generales.

Pros

  • Simple
  • Cubre todos los casos de uso

Contras

  • Ineficiente – podría agotar la batería de un dispositivo móvil
  • Actualizaciones retrasadas hasta la duración del intervalo de votación

Solución #2: ResizeObserver

ResizeObserver es una nueva API que nos notificará cuando el tamaño del elemento cambie.

Pros

  • Eficaz para los navegadores que lo soportan
  • Obtener automáticamente un rendimiento mejorado cuando otros navegadores añaden soporte
  • Bonito API

Contras

  • No proporciona actualizaciones de posición, sólo tamaño
  • Hay que usar un polifilamento

Recursos

Recomendaciones

  • Acepta el hecho de que el tamaño y la posición cambiarán. No los consigas una vez en el componenteDidMount y espera que sean precisos.
  • Guarda el tamaño y la posición del elemento en tu estado. Luego compruebe los cambios a través de ResizeObserver o de las encuestas, dependiendo de sus necesidades.
  • Si utiliza el polling, recuerde que el estado de actualización provoca un ciclo de renderizado incluso si los nuevos valores son los mismos que los antiguos. Por lo tanto, compruebe que el tamaño o la posición realmente cambiaron antes de actualizar su estado.

Un ejemplo práctico con el sondeo

Para mis propios propósitos, necesito no sólo el tamaño, sino también la posición del elemento. Por lo tanto ResizeObserver fue descartado y tuve que ir con una solución de votación. Aquí hay un ejemplo más pacífico de cómo se podría implementar la votación.

En este ejemplo vamos a centrar un elemento dentro de un contenedor. Lo llamo un «tooltip», pero siempre es visible.

classTooltipContainerextendsReact.Componente{constructor(props){super(props);constdefaultRect={izquierda:0,ancho:0};this.state={containerRect:defaultRect,tooltipRect:defaultRect};this. containerRef=React.createRef();this.tooltipRef=React.createRef();this.getRectsInterval=undefined;}componentDidMount(){this.getRectsInterval=setInterval(()= &gt {this.  null:{containerRect};});this.setState(state={{consttooltipRect=esta.tooltipRef.current.getBoundingClientRect();returnJSON.stringify(tooltipRect)===JSON.stringify(state.tooltipRect)? null:{tooltipRect};});},10);}componenteWillUnmount(){clearInterval(this.getRectsInterval);}render(){constleft=este.state.containerRect.left+este.state.containerRect.width/2-this.state.tooltipRect. width/2+"px";return(<divref={this.containerRef}style={{{{visualizar: "en línea-bloque",posición: "relativa"}};<span=;Aquí hay un texto que hará que el padre se expanda</span...;imgsrc="https://www. telegraph.co.uk/content/dam/pets/2017/01/06/1-JS117202740-yana-two-face-cat-news_trans_NvBQzQNjv4BqJNqHJA5DVIMqgv_1zKR2kxRY9bnFVTp4QZlQjJfe6H0.jpg? imwidth=450"/;divref={this.tooltipRef}style={{{{fondo: "azul",posición: "absoluta",top:0,izquierda}}};Tooltip</div></div;);}}

Resumen

Desearía que hubiera una solución perfecta para este problema. Desearía que ResizeObserver fuera soportado por todos los navegadores y proporcionara actualizaciones de posición. Por ahora, me temo que vas a tener que elegir tu veneno.

Categorías: technicalTags: javascript, react, deep dive