CSS 如何在渲染前获取反应组件的大小(高度/宽度)?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/49058890/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 12:52:30  来源:igfitidea点击:

How to get a react component's size (height/width) before render?

cssreactjs

提问by foreyez

I have a react component that needs to know its dimensions ahead of time, before it renders itself.

我有一个 React 组件,它需要在呈现自己之前提前知道它的尺寸

When I'd make a widget in jquery I could just $('#container').width()and get the width of the container ahead of time when I build my component.

当我在 jquery 中制作一个小部件时,我可以$('#container').width()在构建组件时提前获取容器的宽度。

<div id='container'></div>

these container's dimensions are defined in CSS, along with a bunch of other containers on the page. who defines the height and width and placement of the components in React? I'm used to CSS doing that and being able to access that. But in React it seems I can only access that information after the component has rendered.

这些容器的尺寸是在 CSS 中定义的,以及页面上的一堆其他容器。谁定义了 React 中组件的高度和宽度以及位置?我习惯于 CSS 这样做并且能够访问它。但是在 React 中,我似乎只能在组件呈现后才能访问该信息。

采纳答案by Stanko

As it was already mentioned, you can't get any element's dimensions until it is rendered to DOM. What you can do in React is to render only a container element, then get it's size in componentDidMount, and then render rest of the content.

正如已经提到的,在渲染到 DOM 之前,您无法获得任何元素的尺寸。你可以在 React 中做的是只渲染一个容器元素,然后获取它的大小componentDidMount,然后渲染其余的内容。

I made a working example.

我做了一个工作示例

Please note that using setStatein componentDidMountis an anti-pattern but in this case is fine, as it is exactly what are we trying to achieve.

请注意,使用setStateincomponentDidMount是一种反模式,但在这种情况下很好,因为这正是我们想要实现的目标。

Cheers!

干杯!

Code:

代码:

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>
    );
  }
}

回答by Shane

The example below uses react hook useEffect.

下面的示例使用 react 钩子useEffect

Working example 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;

Some Caveats

一些注意事项

useEffectwill not be able to detect it's own influence to width and height

useEffect将无法检测到它自己对宽度和高度的影响

For example if you change the state hook without specifying initial values (eg const [dimensions, setDimensions] = useState({});), the height would read as zero when rendered because

例如,如果您在未指定初始值(例如const [dimensions, setDimensions] = useState({});)的情况下更改状态挂钩,则渲染时高度将读取为零,因为

  • no explicit height was set on the component via css
  • only content drawn before useEffect can be used to measure width and height
  • The only component contents are p tags with the height and width variables, when empty will give the component a height of zero
  • useEffect will not fire again after setting the new state variables.
  • 没有通过 css 在组件上设置明确的高度
  • 只有在 useEffect 之前绘制的内容才能用于测量宽度和高度
  • 唯一的组件内容是带有高度和宽度变量的 p 标签,当为空时,组件的高度为零
  • 设置新的状态变量后,useEffect 不会再次触发。

This is probably not an issue in most use cases, but I thought I would include it because it has implications for window resizing.

在大多数用例中,这可能不是问题,但我想我会包括它,因为它对窗口大小调整有影响。

Window Resizing

调整窗口大小

I also think there are some unexplored implications in the original question. I ran into the issue of window resizingfor dynamically drawn components such as charts.

我还认为原始问题中有一些未探索的含义。我遇到了为动态绘制的组件(例如图表)调整窗口大小问题

I'm including this answer even though it wasn't specifiedbecause

即使没有指定,我也包括这个答案,因为

  1. It's fair to assume that if the dimensions are needed by the application, they will probably be needed on window resize.
  2. Only changes to state or props will cause a redraw, so a window resize listener is also neededto monitor changes to the dimensions
  3. There's a performance hit if you redraw the component on every window resizeevent with more complex components. I found introducing setTimeout and clearInterval helped. My component included a chart, so my CPU spiked and the browser started to crawl. The solution below fixed this for me.
  1. 可以公平地假设,如果应用程序需要尺寸,则在调整窗口大小时可能需要它们。
  2. 只有 state 或 props 的变化才会导致重绘,因此还需要一个窗口大小调整监听器来监控维度的变化
  3. 如果您在每个具有更复杂组件的窗口调整大小事件上重绘组件,则会影响性能。我发现引入 setTimeout 和 clearInterval 有帮助。我的组件包含一个图表,所以我的 CPU 飙升,浏览器开始爬行。下面的解决方案为我解决了这个问题。

code below, working example 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: window resizing timeout- In my case I'm drawing a dashboard with charts downstream from these values and I found 100ms on RESET_TIMEOUTseemed to strike a good balance for me between CPU usage and responsiveness. I have no objective data on what's ideal, so I made this a variable.

回复:窗口调整超时- 在我的情况下,我正在绘制一个带有这些值下游图表的仪表板,我发现 100 毫秒RESET_TIMEOUT似乎在 CPU 使用率和响应性之间取得了很好的平衡。我没有关于什么是理想的客观数据,所以我把它变成了一个变量。

回答by Litty

You cannot. Not reliably, anyway. This is a limitation of browser behavior in general, not React.

你不能。反正不可靠。这通常是浏览器行为的限制,而不是 React。

When you call $('#container').width(), you are querying the width of an element that hasrendered in the DOM. Even in jQuery you can't get around this.

当您调用 时$('#container').width(),您正在查询在 DOM 中呈现的元素的宽度。即使在 jQuery 中,你也无法解决这个问题。

If you absolutely need an element's width before it renders, you will need to estimate it. If you need to measure before being visibleyou can do so while applying visibility: hidden, or render it somewhere discretely on the page then moving it after measurement.

如果您在渲染之前绝对需要元素的宽度,则需要估计它。如果您需要在可见之前进行测量,您可以在应用时进行visibility: hidden,或者在页面上的某个地方离散渲染它,然后在测量后移动它。

回答by Ondra ?i?ka

As stated, it is a limitation of the browsers - they render in one go and "in one thread" (from JS perspective) between your script that manipulates the DOM, and between event handlers execution. To get the dimensions after manipulating / loading the DOM, you need to yield (leave your function) and let the browser render, and react to some event that rendering is done.

如前所述,这是浏览器的一个限制——它们在操作 DOM 的脚本和事件处理程序执行之间一次性和“在一个线程中”(从 JS 的角度)呈现。要在操作/加载 DOM 后获取尺寸,您需要让出(离开您的函数)并让浏览器呈现,并对呈现完成的某些事件做出反应。

But try this trick:
You could try to set CSS display: hidden; position: absolute;and restrict it to some invisible bounding box to get the desired width. Then yield, and when the rendering is done, call $('#container').width().

但是试试这个技巧:
您可以尝试设置 CSSdisplay: hidden; position: absolute;并将其限制为一些不可见的边界框以获得所需的宽度。然后yield,当渲染完成后,调用$('#container').width().

The idea is: Since display: hiddenmakes the element occupy the space it would take if visible, the computation must be done in the background. I am not sure if that qualifies as "before render".

这个想法是:由于display: hidden使元素占据可见的空间,因此计算必须在后台完成。我不确定这是否符合“渲染前”的条件。



Disclaimer:
I haven't tried it, so let me know if it worked.
And I am not sure how it would blend with React.

免责声明:
我还没有尝试过,所以请告诉我它是否有效。
而且我不确定它会如何与 React 融合。

回答by dkloke

@Stanko's solution is nice and terse, but it's post-render. I have a different scenario, rendering a <p>element inside an SVG <foreignObject>(in a Recharts chart). The <p>contains text that wraps, and the final height of the width-constrained <p>is hard to predict. The <foreignObject>is basically a viewport and if too long it would block clicks/taps to underlying SVG elements, too short and it chops off the bottom of the <p>. I need a tight fit, the DOM's own style-determined height before the React render. Also, no JQuery.

@Stanko 的解决方案简洁明了,但它是渲染后的。我有一个不同的场景,<p>在 SVG 中渲染一个元素<foreignObject>(在 Recharts 图表中)。所述<p>包含文本包裹物,和的宽度受限的最终高度<p>是很难预测。的<foreignObject>基本上是一个视域,且如果太长,会阻止点击/抽头到底层SVG元素,太短和它扒关闭的底部<p>。我需要一个紧身,在 React 渲染之前 DOM 自己的样式确定的高度。此外,没有 JQuery。

So in my functional React component I create a dummy <p>node, place it to the live DOM outside the document's client viewport, measure it, and remove it again. Then use that measurement for the <foreignObject>.

因此,在我的功能性 React 组件中,我创建了一个虚拟<p>节点,将其放置到文档客户端视口外的实时 DOM 中,对其进行测量,然后再次将其删除。然后将该测量值用于<foreignObject>.

[Edited with method using CSS classes] [Edited: Firefox hates findCssClassBySelector, stuck with hardcoding for now.]

[使用 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>
}

Ugly, lots of assumptions, probably slow and I'm nervous about the garbage, but it works. Note that the width prop has to be a number.

丑陋,很多假设,可能很慢而且我对垃圾感到紧张,但它有效。请注意,宽度道具必须是一个数字。

回答by RobJacobson

There's an unexpected "gotcha" with @shane's approach for handling window resizing: The functional component adds a new event listener on every re-render, and never removes an event listener, so the number of event listeners grows exponentially with each resize. You can see that by logging each call to window.addEventListener:

@shane 处理窗口大小调整的方法有一个意想不到的“陷阱”:功能组件在每次重新渲染时添加一个新的事件侦听器,并且永远不会删除事件侦听器,因此事件侦听器的数量随着每次调整大小而呈指数增长。您可以通过记录对 window.addEventListener 的每次调用来查看:

window.addEventListener("resize", () => {
  console.log(`Resize: ${dimensions.width} x ${dimensions.height}`);
  clearInterval(movement_timer);
  movement_timer = setTimeout(test_dimensions, RESET_TIMEOUT);
});

This could be fixed by using an event cleanup pattern. Here's some code that's a blend of @shane's code and this tutorial, with the resizing logic in a custom hook:

这可以通过使用事件清理模式来解决。这里有一些代码混合了@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;

There's a working example here.

有一个工作示例这里

This code doesn't use a timer, for simplicity, but that approach is further discussed in the linked tutorial.

为简单起见,此代码不使用计时器,但在链接教程中进一步讨论了该方法。