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

时间:2018-03-01 21:22:14

标签: css reactjs

我有一个反应组件,需要提前知道它的尺寸,才能渲染自己。

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

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

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

7 个答案:

答案 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({});),则高度在渲染时将显示为零,因为

  • 未通过css在组件上设置明确的高度
  • 仅在useEffect之前绘制的内容可用于测量宽度和高度
  • 唯一的组件内容是带有高度和宽度变量的p标签,如果为空,则组件的高度为零。
  • 设置新的状态变量后,
  • useEffect将不会再次触发。

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

调整窗口大小

我还认为原始问题中存在一些未开发的含义。我遇到了调整窗口大小的问题,以动态绘制诸如图表之类的组件。

即使未指定,我也包含此答案,因为

  
      
  1. 可以公平地假设,如果应用程序需要这些尺寸,则可能需要在调整窗口大小时使用它们。
  2.   
  3. 仅状态或道具的更改会导致重绘,因此还需要窗口调整大小的侦听器来监视尺寸的变化
  4.   
  5. 如果在每次使用更复杂组件的窗口调整大小事件上重新绘制组件,都会对性能产生影响。我发现   引入setTimeout和clearInterval帮助。我的组件   包含一张图表,所以我的CPU猛增,浏览器开始爬行。   下面的解决方案为我解决了这个问题。
  6.   

下面的代码,工作示例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>
}

丑陋的假设很多,可能很慢,我对垃圾感到不安,但它确实有效。请注意,宽度属性必须为数字。