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

IT技术 css reactjs
2021-04-21 12:07:10

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

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

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

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

6个回答

下面的示例使用 react 钩子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 在组件上设置明确的高度
  • 只有在 useEffect 之前绘制的内容才能用于测量宽度和高度
  • 唯一的组件内容是带有高度和宽度变量的 p 标签,当为空时,组件的高度为零
  • 设置新的状态变量后,useEffect 不会再次触发。

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

调整窗口大小

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

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

  1. 可以公平地假设,如果应用程序需要尺寸,则在调整窗口大小时可能需要它们。
  2. 只有 state 或 props 的变化才会导致重绘,因此还需要一个窗口大小调整监听器来监控维度的变化
  3. 如果在每个具有更复杂组件的窗口调整大小事件上重绘组件,则会影响性能我发现引入 setTimeout 和 clearInterval 有帮助。我的组件包含一个图表,所以我的 CPU 飙升,浏览器开始爬行。下面的解决方案为我解决了这个问题。

下面的代码,这里的工作示例

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似乎在 CPU 使用率和响应能力之间取得了很好的平衡。我没有关于什么是理想的客观数据,所以我把它变成了一个变量。

这里有一个很好的例子来调整stackoverflow.com/questions/19014250/...
2021-05-28 12:07:10
嗨,我分叉了您的沙箱并更改了我监视父子事件的位置。我试图根据从父级传递的宽度限制来限制我在子级上显示的项目的大小。知道如何做到这一点吗?codeandbox.io/s/…
2021-05-29 12:07:10
@VergilC。- 您的意思是要限制基于父级显示的按钮数量或基于父级的每个按钮的尺寸?这实际上取决于您希望基于什么显示规则。像......maxCount = Math.floor(parentWidth / buttonWidth); displayCount = (maxCount > availableCount) ? availableCount : maxCount;如果我理解正确的话。
2021-06-02 12:07:10

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

我做了一个工作示例

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

干杯!

代码:

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>
    );
  }
}
使用setStateincomponentDidMount不是一种反模式,只是有一些警告。参见:stackoverflow.com/a/52168388/6817437
2021-05-29 12:07:10

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

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

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

@TJBlackman 它直接引用了这个问题。他们特别提到他们可以在 jQuery 中做到这一点,我解决了这个问题,说 jQuery 不能。
2021-05-23 12:07:10
“即使在 jQuery 中,你也无法解决这个问题。” 是的,甚至你强大的 JQuery 也不是!所有框架的教父,全球 jr 开发者的光明使者,网络的不朽守护者和工作马......因为即使是无所不能的 JQuery 也无法摆脱这种限制!
2021-06-11 12:07:10
oop:) 对不起
2021-06-12 12:07:10
哦,是的,我知道……我只是在开玩笑!没有伤害的意思!;)
2021-06-20 12:07:10

@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;

有一个工作示例这里

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

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

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

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


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