我有一个反应组件,需要提前知道它的尺寸,才能渲染自己。
当我在jquery中创建一个小部件时,我可以$('#container').width()
并在构建组件时提前获取容器的宽度。
<div id='container'></div>
这些容器的尺寸在CSS中定义,以及页面上的一堆其他容器。谁在React中定义了组件的高度和宽度以及位置?我已经习惯了CSS,并能够访问它。但在React中,我似乎只能在组件渲染后访问该信息。
答案 0 :(得分:16)
下面的示例使用了React钩子useEffect 。
工作示例here
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({});
),则高度在渲染时将显示为零,因为
在大多数用例中,这可能不是问题,但我认为我应该将其包括在内,因为它对调整窗口大小有影响。
我还认为原始问题中存在一些未开发的含义。我遇到了调整窗口大小的问题,以动态绘制诸如图表之类的组件。
即使未指定,我也包含此答案,因为
- 可以公平地假设,如果应用程序需要这些尺寸,则可能需要在调整窗口大小时使用它们。
- 仅状态或道具的更改会导致重绘,因此还需要窗口调整大小的侦听器来监视尺寸的变化
- 如果在每次使用更复杂组件的窗口调整大小事件上重新绘制组件,都会对性能产生影响。我发现 引入setTimeout和clearInterval帮助。我的组件 包含一张图表,所以我的CPU猛增,浏览器开始爬行。 下面的解决方案为我解决了这个问题。
下面的代码,工作示例here
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:窗口大小调整超时 -在我的情况下,我正在绘制一个仪表盘,这些值的下游是图表,我发现RESET_TIMEOUT
上的时间为100ms在CPU使用率和响应能力之间取得了很好的平衡。我没有关于理想情况的客观数据,所以我将其设置为变量。
答案 1 :(得分:11)
正如已经提到的,在呈现给DOM之前,你无法获得任何元素的维度。你在React中可以做的只是渲染一个容器元素,然后在componentDidMount
中获取它的大小,然后渲染其余的内容。
我做了working example。
请注意,在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>
);
}
}
答案 2 :(得分:5)
@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的代码和this tutorial的混合代码,在custom hook中带有调整大小逻辑:
/* 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;
有一个有效的示例here。
为简单起见,此代码不使用计时器,但是在链接的教程中将进一步讨论该方法。
答案 3 :(得分:3)
你做不到。无论如何,还不可靠。这是浏览器行为的一般限制,而不是React。
当您致电$('#container').width()
时,您正在查询在DOM中呈现 的元素的宽度。即使在jQuery中你也无法解决这个问题。
如果您在渲染之前绝对需要元素的宽度,则需要对其进行估算。如果您需要在可见之前进行测量,您可以在应用visibility: hidden
时进行测量,或者在页面上离散地渲染它,然后在测量后移动它。
答案 4 :(得分:1)
如上所述,这是浏览器的一个局限性-它们在操作DOM的脚本之间以及事件处理程序执行之间一次性(从JS角度)渲染并“在一个线程中”渲染。要在操作/加载DOM之后获取尺寸,您需要让步(离开函数)并让浏览器渲染,并对完成渲染的事件做出反应。
但是请尝试以下技巧:
您可以尝试设置CSS display: hidden; position: absolute;
并将其限制为不可见的边界框,以获取所需的宽度。然后屈服,渲染完成后,调用$('#container').width()
。
想法是:由于display: hidden
使元素占据了它在可见时所需要的空间,因此必须在后台进行计算。
我不确定这是否符合“渲染前”的条件。
免责声明:
我还没有尝试过,所以让我知道它是否有效。
而且我不确定它将如何与React融合。
答案 5 :(得分:1)
我在 Stack Overflow 上找到的所有解决方案要么非常慢,要么与现代 React 约定不符。然后我偶然发现:
https://github.com/wellyshen/react-cool-dimensions
<块引用>一个 React 钩子,它使用 ResizeObserver 以高性能的方式测量元素的大小并处理响应式组件。
与我在这里尝试的解决方案相比,它速度快且效果更好。
答案 6 :(得分:0)
@Stanko的解决方案简洁明了,但是是后期渲染的。我有另一种情况,在SVG <p>
内(在“图表”图中)呈现了<foreignObject>
元素。 <p>
包含自动换行的文本,受宽度限制的<p>
的最终高度很难预测。 <foreignObject>
基本上是一个视口,如果时间过长,则会阻止对基础SVG元素的点击/敲击,过短则会阻塞<p>
的底部。我需要紧密配合,在React渲染之前DOM由样式确定的高度。另外,没有JQuery。
因此,在我的功能性React组件中,我创建了一个虚拟<p>
节点,将其放置在文档客户端视口之外的实时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>
}
丑陋的假设很多,可能很慢,我对垃圾感到不安,但它确实有效。请注意,宽度属性必须为数字。