Как получить размер реагирующего компонента (высота / ширина) перед рендерингом?
У меня есть компонент реагирования, который должен знать свои размеры заранее, до того, как он проявит себя.
Когда я сделал виджет в jquery, я мог просто $('#container').width()
и получить ширину контейнера заранее, когда я соберу свой компонент.
<div id='container'></div>
Размеры этих контейнеров определены в CSS вместе с кучей других контейнеров на странице. кто определяет высоту и ширину и расположение компонентов в React? Я привык к тому, что CSS делает это и может получить к нему доступ. Но в React кажется, что я могу получить доступ к этой информации только после рендеринга компонента.
8 ответов
Как уже упоминалось, вы не можете получить размеры элемента, пока он не будет отображен в DOM. В React вы можете визуализировать только элемент контейнера, а затем получить его размер componentDidMount
, а затем визуализировать остальную часть содержимого.
Я сделал рабочий пример.
Обратите внимание, что с помощью setState
в componentDidMount
это анти-паттерн, но в данном случае это нормально, потому что это именно то, чего мы пытаемся достичь.
Ура!
Код:
import React, { Component } from 'react';
export default class Example extends Component {
state = {
dimensions: null,
};
componentDidMount() {
this.setState({
dimensions: {
width: this.container.offsetWidth,
height: this.container.offsetHeight,
},
});
}
renderContent() {
const { dimensions } = this.state;
return (
<div>
width: {dimensions.width}
<br />
height: {dimensions.height}
</div>
);
}
render() {
const { dimensions } = this.state;
return (
<div className="Hello" ref={el => (this.container = el)}>
{dimensions && this.renderContent()}
</div>
);
}
}
В приведенном ниже примере используется перехватчик реакции useEffect.
Рабочий пример здесь
import React, { useRef, useLayoutEffect, useState } from "react";
const ComponentWithDimensions = props => {
const targetRef = useRef();
const [dimensions, setDimensions] = useState({ width:0, height: 0 });
useLayoutEffect(() => {
if (targetRef.current) {
setDimensions({
width: targetRef.current.offsetWidth,
height: targetRef.current.offsetHeight
});
}
}, []);
return (
<div ref={targetRef}>
<p>{dimensions.width}</p>
<p>{dimensions.height}</p>
</div>
);
};
export default ComponentWithDimensions;
Некоторые предостережения
useEffect не сможет определить собственное влияние на ширину и высоту
Например, если вы измените обработчик состояния без указания начальных значений (например, const [dimensions, setDimensions] = useState({});
), при рендеринге высота будет считаться нулевой, потому что
- Для компонента не была установлена явная высота через css
- только контент, нарисованный перед использованием Эффект может использоваться для измерения ширины и высоты
- Единственное содержимое компонента - это теги p с переменными высоты и ширины, пустое значение даст компоненту нулевую высоту.
- useEffect не сработает снова после установки новых переменных состояния.
Вероятно, это не проблема в большинстве случаев использования, но я подумал, что включу его, потому что это влияет на изменение размера окна.
Изменение размера окна
Я также думаю, что в исходном вопросе есть некоторые неисследованные последствия. Я столкнулся с проблемой изменения размера окна для динамически отображаемых компонентов, таких как диаграммы.
Я включаю этот ответ, хотя он не был указан, потому что
- Справедливо предположить, что если размеры необходимы приложению, они, вероятно, потребуются при изменении размера окна.
- Только изменения состояния или свойств будут вызывать перерисовку, поэтому слушатель изменения размера окна также необходим для отслеживания изменений размеров
- Производительность падает, если вы перерисовываете компонент при каждом событии изменения размера окна с более сложными компонентами. Я обнаружил, что нам помогли setTimeout и clearInterval. Мой компонент включал в себя диаграмму, поэтому мой процессор начал работать, и браузер начал сканировать. Приведенное ниже решение исправило это для меня.
код ниже, рабочий пример здесь
import React, { useRef, useLayoutEffect, useState } from 'react';
const ComponentWithDimensions = (props) => {
const targetRef = useRef();
const [dimensions, setDimensions] = useState({});
// holds the timer for setTimeout and clearInterval
let movement_timer = null;
// the number of ms the window size must stay the same size before the
// dimension state variable is reset
const RESET_TIMEOUT = 100;
const test_dimensions = () => {
// For some reason targetRef.current.getBoundingClientRect was not available
// I found this worked for me, but unfortunately I can't find the
// documentation to explain this experience
if (targetRef.current) {
setDimensions({
width: targetRef.current.offsetWidth,
height: targetRef.current.offsetHeight
});
}
}
// This sets the dimensions on the first render
useLayoutEffect(() => {
test_dimensions();
}, []);
// every time the window is resized, the timer is cleared and set again
// the net effect is the component will only reset after the window size
// is at rest for the duration set in RESET_TIMEOUT. This prevents rapid
// redrawing of the component for more complex components such as charts
window.addEventListener('resize', ()=>{
clearInterval(movement_timer);
movement_timer = setTimeout(test_dimensions, RESET_TIMEOUT);
});
return (
<div ref={ targetRef }>
<p>{ dimensions.width }</p>
<p>{ dimensions.height }</p>
</div>
);
}
export default ComponentWithDimensions;
re: тайм-аут изменения размера окна - в моем случае я рисую панель мониторинга с диаграммами ниже этих значений, и я нашел 100 мс наRESET_TIMEOUT
мне показалось, что я нашел хороший баланс между загрузкой процессора и быстродействием. У меня нет объективных данных о том, что идеально, поэтому я сделал это переменной.
Ты не можешь. Во всяком случае, ненадежно. Это ограничение поведения браузера в целом, а не React.
Когда вы звоните $('#container').width()
вы запрашиваете ширину элемента, который отображается в DOM. Даже в jQuery вы не можете обойти это.
Если вам абсолютно необходима ширина элемента перед его рендерингом, вам необходимо оценить его. Если вам нужно измерить, прежде чем быть видимым, вы можете сделать это во время применения visibility: hidden
или визуализировать его где-нибудь на странице, а затем переместить после измерения.
В подходе @shane к изменению размера окна есть неожиданная "ловушка": функциональный компонент добавляет новый прослушиватель событий при каждом повторном рендеринге и никогда не удаляет прослушиватель событий, поэтому количество прослушивателей событий растет экспоненциально с каждым изменением размера. Вы можете убедиться в этом, регистрируя каждый вызов window.addEventListener:
window.addEventListener("resize", () => {
console.log(`Resize: ${dimensions.width} x ${dimensions.height}`);
clearInterval(movement_timer);
movement_timer = setTimeout(test_dimensions, RESET_TIMEOUT);
});
Это можно исправить, используя шаблон очистки событий. Вот код, который представляет собой смесь кода @shane и этого руководства с логикой изменения размера в настраиваемом хуке:
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect, useLayoutEffect, useRef } from "react";
// Usage
function App() {
const targetRef = useRef();
const size = useDimensions(targetRef);
return (
<div ref={targetRef}>
<p>{size.width}</p>
<p>{size.height}</p>
</div>
);
}
// Hook
function useDimensions(targetRef) {
const getDimensions = () => {
return {
width: targetRef.current ? targetRef.current.offsetWidth : 0,
height: targetRef.current ? targetRef.current.offsetHeight : 0
};
};
const [dimensions, setDimensions] = useState(getDimensions);
const handleResize = () => {
setDimensions(getDimensions());
};
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
useLayoutEffect(() => {
handleResize();
}, []);
return dimensions;
}
export default App;
Там это рабочий пример здесь.
В этом коде не используется таймер для простоты, но этот подход дополнительно обсуждается в связанном руководстве.
Как уже говорилось, это ограничение браузеров - они визуализируются за один раз и "в одном потоке" (с точки зрения JS) между вашим скриптом, который манипулирует DOM, и между выполнением обработчиков событий. Чтобы получить размеры после манипулирования / загрузки DOM, вам нужно уступить (оставить свою функцию) и позволить браузеру рендериться и реагировать на какое-то событие, когда рендеринг выполняется.
Но попробуйте этот трюк:
Вы можете попробовать установить CSS display: hidden; position: absolute;
и ограничить его неким невидимым ограничивающим прямоугольником, чтобы получить желаемую ширину. Затем уступите, и когда рендеринг будет завершен, вызовите $('#container').width()
,
Идея такова: с display: hidden
заставляет элемент занимать пространство, которое он занимал бы, если бы был видим, вычисления должны выполняться в фоновом режиме. Я не уверен, что это квалифицируется как "перед рендерингом".
Отказ от ответственности:
Я не пробовал, так что дайте мне знать, если это сработало.
И я не уверен, как это будет сочетаться с React.
Все решения, которые я нашел в Stack overflow, были либо очень медленными, либо устаревшими с современными соглашениями React. Затем я наткнулся на:
https://github.com/wellyshen/react-cool-dimensions
Хук React, который измеряет размер элемента и обрабатывает быстро реагирующие компоненты с помощью ResizeObserver.
Это быстро и работает намного лучше, чем решения, которые я здесь пробовал.
Решение @Stanko красивое и краткое, но это пост-рендер. У меня другой сценарий, рендеринг<p>
элемент внутри SVG <foreignObject>
(в диаграмме Recharts). В<p>
содержит текст, который оборачивается, и окончательная высота ограниченного по ширине <p>
сложно предсказать. В<foreignObject>
в основном является окном просмотра, и если он будет слишком длинным, он будет блокировать клики / касания к базовым элементам SVG, слишком короткий, и он отрубает нижнюю часть <p>
. Мне нужна плотная посадка, высота, определяемая собственным стилем DOM, до рендеринга React. Кроме того, нет JQuery.
Итак, в моем функциональном компоненте React я создаю фиктивный <p>
node, поместите его в действующую модель DOM за пределами клиентского окна просмотра документа, измерьте его и снова удалите. Затем используйте это измерение для<foreignObject>
.
[Отредактировано методом с использованием классов CSS] [Отредактировано: Firefox ненавидит findCssClassBySelector, пока придерживается жесткого кодирования.]
const findCssClassBySelector = selector => [...document.styleSheets].reduce((el, f) => {
const peg = [...f.cssRules].find(ff => ff.selectorText === selector);
if(peg) return peg; else return el;
}, null);
// find the class
const eventLabelStyle = findCssClassBySelector("p.event-label")
// get the width as a number, default 120
const eventLabelWidth = eventLabelStyle && eventLabelStyle.style ? parseInt(eventLabelStyle.style.width) : 120
const ALabel = props => {
const {value, backgroundcolor: backgroundColor, bordercolor: borderColor, viewBox: {x, y}} = props
// create a test DOM node, place it out of sight and measure its height
const p = document.createElement("p");
p.innerText = value;
p.className = "event-label";
// out of sight
p.style.position = "absolute";
p.style.top = "-1000px";
// // place, measure, remove
document.body.appendChild(p);
const {offsetHeight: calcHeight} = p; // <<<< the prize
// does the DOM reference to p die in garbage collection, or with local scope? :p
document.body.removeChild(p);
return <foreignObject {...props} x={x - eventLabelWidth / 2} y={y} style={{textAlign: "center"}} width={eventLabelWidth} height={calcHeight} className="event-label-wrapper">
<p xmlns="http://www.w3.org/1999/xhtml"
className="event-label"
style={{
color: adjustedTextColor(backgroundColor, 125),
backgroundColor,
borderColor,
}}
>
{value}
</p>
</foreignObject>
}
Уродливо, много предположений, наверное, медленно, и я нервничаю по поводу мусора, но это работает. Обратите внимание, что опора ширины должна быть числом.
import { useState, useEffect } from 'react'
const useContainerDimensions = containerRef => {
const getDimensions = () => ({
width: containerRef.current.offsetWidth,
height: containerRef.current.offsetHeight
})
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
useEffect(() => {
const handleResize = () => {
setDimensions(getDimensions())
}
let dimensionsTimeout = setTimeout(() => {
if(containerRef.current) {
setDimensions(getDimensions())
}
}, 100)
window.addEventListener("resize", handleResize)
return () => {
clearTimeout(dimensionsTimeout)
window.removeEventListener("resize", handleResize)
}
}, [containerRef])
return dimensions
}
export default useContainerDimensions
Вы можете использовать пользовательский хук useContainerDimensions. если вам нужна ширина и высота в пикселях, вы можете использовать clientWidth и clientHeight вместо offsetWidth и offsetHeight.