如何获取React元素的宽度

152

我正在尝试创建一个范围输入框,它可以在滑块拇指右上方显示工具提示。

我查阅了一些在线的纯JavaScript示例,似乎需要获取元素的宽度才能实现这个功能。

因此,我想知道如何获取元素的宽度?

基本上等同于jQuery方法$(element).width()


13
2020年,我强烈推荐使用react-use中的useMeasure()代替。它返回宽度、高度、x位置、y位置等更多信息。 - Christopher Regner
14个回答

142
    class MyComponent extends Component {
      constructor(props){
        super(props)
        this.myInput = React.createRef()
      }

      componentDidMount () {
        console.log(this.myInput.current.offsetWidth)
      }

      render () {
        return (
        // new way - as of React@16.3
        <div ref={this.myInput}>some elem</div>
        // legacy way
        // <div ref={(ref) => this.myInput = ref}>some elem</div>
        )
      }
    }

8
我想说的是,显然现在创建引用的方式也已经过时了。(现在你可以使用 React.createRef() 在构造函数中创建引用) - aaaidan
5
刚刚查看了文档。回调引用并没有被弃用。有时候在不想覆盖克隆元素上的引用时,你需要使用它们。 - user1164937
你想获取一个 React 元素而不是原生 HTML 元素的 Div 元素的宽度吗? - RezKesh
2
感谢您维护“传统”的方式。 - skwidbreth

116

使用hooks

const MyComponent = () => {
  const ref = useRef(null);
  useEffect(() => {
    console.log('width', ref.current ? ref.current.offsetWidth : 0);
  }, [ref.current]);
  return <div ref={ref}>Hello</div>;
};

在上面的示例中,您可以使用回调引用来替代同时使用useRefuseEffect - Izhaki
21
但是当任何调整大小发生时,它不会得到更新,只有在ref.current更改时才会更新。 - Marco Antônio
12
那个完美运作。另外,使用Typescript只需添加const ref = useRef<HTMLHeadingElement>(null); - Albert Tjornejoj
7
Eslint 报错:React Hook useEffect 有一个不必要的依赖:'ref.current'。要么排除它,要么将其从依赖数组中移除。像 'ref.current' 这样的可变值不是有效的依赖项,因为对它们进行突变不会重新渲染组件。 - Ali Mert Çakar
3
我会转而使用 useLayoutEffect,并且要么完全跳过 , [ref.current](以便每次渲染都更新它),要么添加一个 ResizeObserver 或其他东西来在调整大小时更新。 - Svish

61

这基本上是Marco Antônio在React自定义钩子方面的答案,但进行了修改以在调整大小之前设置尺寸。

export const useContainerDimensions = myRef => {
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 })

  useEffect(() => {
    const getDimensions = () => ({
      width: myRef.current.offsetWidth,
      height: myRef.current.offsetHeight
    })

    const handleResize = () => {
      setDimensions(getDimensions())
    }

    if (myRef.current) {
      setDimensions(getDimensions())
    }

    window.addEventListener("resize", handleResize)

    return () => {
      window.removeEventListener("resize", handleResize)
    }
  }, [myRef])

  return dimensions;
};

用法相同:

const MyComponent = () => {
  const componentRef = useRef()
  const { width, height } = useContainerDimensions(componentRef)

  return (
    <div ref={componentRef}>
      <p>width: {width}px</p>
      <p>height: {height}px</p>
    <div/>
  )
}

这到底对任何人起作用吗?在useEffect的第二个参数中,myRef是相同的(在第一次渲染时'current'为null),并且会发生变化,但是useEffect不会再次被调用,因为myRef实际上没有改变。 - Martin.

48

实际上,将此调整大小逻辑隔离在一个自定义钩子中会更好。您可以像这样创建一个自定义钩子:

实际上,最好将此调整大小逻辑隔离到自定义钩子中。 您可以像这样创建自定义钩子:

const useResize = (myRef) => {
  const [width, setWidth] = useState(0)
  const [height, setHeight] = useState(0)
  
  const handleResize = useCallback(() => {
      setWidth(myRef.current.offsetWidth)
      setHeight(myRef.current.offsetHeight)
  }, [myRef])

  useEffect(() => {
    window.addEventListener('load', handleResize)
    window.addEventListener('resize', handleResize)

    return () => {
      window.removeEventListener('load', handleResize)
      window.removeEventListener('resize', handleResize)
    }
  }, [myRef, handleResize])

  return { width, height }
}

然后你可以像这样使用它:

const MyComponent = () => {
  const componentRef = useRef()
  const { width, height } = useResize(componentRef)

  return (
    <div ref={componentRef }>
      <p>width: {width}px</p>
      <p>height: {height}px</p>
    <div/>
  )
}

1
你必须将resize事件侦听器附加到窗口上,我想:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/resize_event - Gerbus
handleResize() on the EventListeners or else useResize will return { 0, 0} - kcNeko
5
不返回初始大小。即只有在调整大小后才起作用。要修复 useResize 需要包括 if (myRef.current) {<setWidth SetHeight 等>) - meesern
1
关于此信息,使用 addEventListener('load', ..) 可以适用于初始大小。 - Shautieh
1
这对任何人都起作用吗?在 useEffect 的第二个参数中,myRef 是相同的(在第一次渲染时 'current' 为 null),但是 myRef 发生了变化,但由于 myRef 实际上没有改变,所以 useEffect 没有被再次调用。 - Martin.

18

一种简单而现代的解决方案是使用React的 useRef hook 来存储组件/元素的引用,再配合一个useEffect hook,在组件 Render 时触发。

import React, {useState, useEffect, useRef} from 'react';

export default App = () => {
  const [width, setWidth] = useState(0);
  const elementRef = useRef(null);

  useEffect(() => {
    setWidth(elementRef.current.getBoundingClientRect().width);
  }, []); //empty dependency array so it only runs once at render

  return (
    <div ref={elementRef}>
      {width}
    </div>
  )
}

我得到了“columnRef.current.getBoundingRect不是一个函数”的错误。 - user3808307
2
由于某些原因,在首次渲染时,引用未绑定到元素。一个简单的解决方法是在使用引用之前添加 if (!ref.current) return;,并将引用添加到依赖项数组中,这样它将变成 [ref] 而不是空数组。 - charri
4
不是getBoundingRect,而是getBoundingClientRect()。 - Douglas Schmidt

14
这是一个TypeScript版本的@meseern答案的翻译,它避免了不必要的重新渲染分配:

这里是一个TypeScript版本的@meseern的答案,它避免了不必要的重新渲染分配:

import React, { useState, useEffect } from 'react';

export function useContainerDimensions(myRef: React.RefObject<any>) {
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

  useEffect(() => {
    const getDimensions = () => ({
      width: (myRef && myRef.current.offsetWidth) || 0,
      height: (myRef && myRef.current.offsetHeight) || 0,
    });

    const handleResize = () => {
      setDimensions(getDimensions());
    };

    if (myRef.current) {
      setDimensions(getDimensions());
    }

    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, [myRef]);

  return dimensions;
}

嗨!如果它是受控组件怎么办? - Antenaina

10

React 18.x将于2023年面世

出于好的原因,React 18改变了useEffect的工作方式。对于一个组件来说只运行一次初始化代码是有效的,但在使用useEffect之前,请先阅读You might not need an effect。要获取元素的尺寸,我们可以使用新的useSyncExternalStore钩子-

// useDimensions.js

import { useMemo, useSyncExternalStore } from "react"

function subscribe(callback) {
  window.addEventListener("resize", callback)
  return () => {
    window.removeEventListener("resize", callback)
  }
}

function useDimensions(ref) {
  const dimensions = useSyncExternalStore(
    subscribe,
    () => JSON.stringify({
      width: ref.current?.offsetWidth ?? 0, // 0 is default width
      height: ref.current?.offsetHeight ?? 0, // 0 is default height
    })
  )
  return useMemo(() => JSON.parse(dimensions), [dimensions])
}

export { useDimensions }

你可以这样使用 -
function MyComponent() {
  const ref = useRef(null)
  const {width, height} = useDimensions(ref)
  return <div ref={ref}>
    The dimensions of this div is {width} x {height}
  </div>
}

为什么要使用JSON.stringify?

useSyncExternalStore 函数期望 getSnapshot 函数返回一个缓存的值,否则会导致无限次重新渲染。

{width: 300, height: 200} === {width: 300, height: 200}
// => false ❌

JSON.stringify 将对象转换为字符串,以便建立相等关系 -

'{"width":300,"height":200}' === '{"width":300,"height":200}'
// => true ✅

最后,useMemo钩子确保在后续渲染中返回相同的尺寸对象。当dimensions字符串发生变化时,记忆会更新,并且使用useDimensions的组件将重新渲染。

立即可用的尺寸

这里的其他答案要求用户触发resize事件才能访问尺寸。有些人尝试使用useEffect内部的手动调用来减轻此问题,但是这些解决方案在React 18中失败了。对于使用useSyncExternalState的此解决方案不是这种情况。享受在第一次渲染时立即访问尺寸的便利!

typescript

这是适用于typescript用户的useDimensions hook-

import { RefObject, useMemo, useSyncExternalStore } from "react"

function subscribe(callback: (e: Event) => void) {
  window.addEventListener("resize", callback)
  return () => {
    window.removeEventListener("resize", callback)
  }
}

function useDimensions(ref: RefObject<HTMLElement>) {
  const dimensions = useSyncExternalStore(
    subscribe,
    () => JSON.stringify({
      width: ref.current?.offsetWidth ?? 0,
      height: ref.current?.offsetHeight ?? 0,
    })
  )
  return useMemo(() => JSON.parse(dimensions), [dimensions])
}

export { useDimensions }

这可以通过SVG元素来实现吗?例如,SVGTextElement?也许可以在TypeScript中进行泛化,以指定元素类型,而不是硬编码<HTMLElement>? - undefined

7

如果您只需要获取React元素的宽度,则可以使用此解决方案:

扩充Christopher的评论: 您可以使用'react-use'库来实现此功能,该库还可侦听浏览器的调整大小。有关参考信息:https://github.com/streamich/react-use/blob/master/docs/useMeasure.md

import React from 'react';

import { useMeasure } from 'react-use'; // or just 'react-use-measure'

const sampleView = () => {
 const [ref, { width }] = useMeasure<HTMLDivElement>();
 console.log('Current width of element', width);
 return <div ref={ref}></div>;
};

export default sampleView;

1
或者,如果您只需要 useMeasure,请访问 https://github.com/pmndrs/react-use-measure。 - GG.

4

有一个库,use-resize-observer,它为您提供了一个基于ResizeObserver构建的钩子。

import React from "react";
import useResizeObserver from "use-resize-observer";

const App = () => {
  const { ref, width, height } = useResizeObserver<HTMLDivElement>();

  return (
    <div>
      <div className="instructions">Try resizing this div!</div>
      <div ref={ref} className="box">
        {width}x{height}
      </div>
    </div>
  );
};

4

React Hook:

import React, { useState, useEffect,useRef } from 'react';
...
const table1ref = useRef(null);
const [table1Size, table1SizeSet] = useState({
  width: undefined,
  height: undefined,
});

useEffect(() => {
    function handleResize() {
      table1SizeSet({
        width: table1ref.current.offsetWidth,
        height: table1ref.current.offsetHeight,
      });
    }
    window.addEventListener("resize", handleResize);
    handleResize();
    return () => window.removeEventListener("resize", handleResize);        
  }, [ ]);
...
<div  ref={table1ref}>

并调用:

{table1Size.width}

当你想要使用时。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接