Coder Social home page Coder Social logo

react-use-measure's Introduction

yarn add react-use-measure

This small tool will measure the boundaries (for instance width, height, top, left) of a view you reference. It is reactive and responds to changes in size, window-scroll and nested-area-scroll.

Why do we need this hook?

Because there is no simple way to just get relative view coordinates. Yes, there is getBoundingClientRect, but it does not work when your content sits inside scroll areas whose offsets are simply neglected (as well as page scroll). Worse, mouse coordinates are relative to the viewport (the visible rect that contains the page). There is no easy way, for instance, to know that the mouse hovers over the upper/left corner of an element. This hook solves it for you.

You can try a live demo here: https://codesandbox.io/s/musing-kare-4fblz

Usage

import useMeasure from 'react-use-measure'

function App() {
  const [ref, bounds] = useMeasure()

  // consider that knowing bounds is only possible *after* the view renders
  // so you'll get zero values on the first run and be informed later

  return <div ref={ref} />
}

Api

interface RectReadOnly {
  readonly x: number
  readonly y: number
  readonly width: number
  readonly height: number
  readonly top: number
  readonly right: number
  readonly bottom: number
  readonly left: number
}

type Options = {
  // Debounce events in milliseconds
  debounce?: number | { scroll: number; resize: number }
  // React to nested scroll changes, don't use this if you know your view is static
  scroll?: boolean
  // You can optionally inject a resize-observer polyfill
  polyfill?: { new (cb: ResizeObserverCallback): ResizeObserver }
  // Measure size using offsetHeight and offsetWidth to ignore parent scale transforms
  offsetSize?: boolean
}

useMeasure(
  options: Options = { debounce: 0, scroll: false }
): [React.MutableRefObject<HTMLElement | SVGElement>, RectReadOnly]

⚠️ Notes

Resize-observer polyfills

This lib relies on resize-observers. If you need a polyfill you can either polute the window object or inject it cleanly using the config options. We recommend @juggle/resize-observer.

import { ResizeObserver } from '@juggle/resize-observer'

function App() {
  const [ref, bounds] = useMeasure({ polyfill: ResizeObserver })

Multiple refs

useMeasure currently returns its own ref. We do this because we are using functional refs for unmount tracking. If you need to have a ref of your own on the same element, use react-merge-refs.

react-use-measure's People

Contributors

aleclarson avatar drcmda avatar everweij avatar huntercaron avatar matoilic avatar tjallingt avatar williaster avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-use-measure's Issues

X position doesn't update when siblings change size

Hi, I just recently started using this library, and really enjoy it so far, but discovered one limitation with it:

https://codesandbox.io/s/fervent-maxwell-c24yg

If you take a look at the above sandbox, and try to type in the input, you'll notice that the x value doesn't get updated straight away. Once you scroll up and down, then it will get updated due to the measurement on scroll.

I'm guessing this might be a limitation of the ResizeObserver API itself? Since the element itself is not resizing?

Any ideas if there might be an API we can use in combination with ResizeObserver to get updates for changes in position?

Thanks!

This will cause hard to find memory leaks in IE11

The dependant package resize-observer-polyfill has not been updated in some time. This is also a dependency in a measure library I have used previously, and it was ultimately the cause for a serious and hard to find memory leak in IE11 -- which persist across page reloads and ends in a crash. See que-etc/resize-observer-polyfill#7 (comment). This lib was also responsible for a crash wheh using the popular ag-grid lib: ag-grid/ag-grid#2487.

Whilst this is ultimately an IE11 issue, the use of this library will inevitably mean that react-use-measure can trigger a serious crash issue in IE11.

The more recent and competing lib: https://github.com/juggle/resize-observer leaves it up to the consumer if to force polyfill on or use a global. This means "force polyfill on" (avoiding mem leak issues) could be an option of the hook.

Don't paint before measurements are taken

On the first render, bounds is obviously filled with 0's.

Is it possible for the 2nd render, with the bounds object filled properly, to happen before the browser paints?

For example, given you want to sync two div's height, the following implementation (with react-use-measure) lets the browser paint after the first render, where the height is 0. Only the second render the synced div jumps to its correct height, which results in a flash.

function Component() {

  const [ref, bounds] = useMeasure();

  return (
    <>
      <div
        ref={ref}
        style={{ backgroundColor: "red", width: "200px", height: "200px" }}
      />
      <div
        style={{
          backgroundColor: "blue",
          width: "200px",
          height: `${bounds.width}px`
        }}
      />
    </>
  );
}

A component like this one, without react-use-measure works just fine, without the flash (properly).

function Component() {
  const ref = useRef<any>();
  const [height, setHeight] = useState(0);

  useLayoutEffect(() => {
    const ro = new ResizeObserver(([entry], observer) => {
      console.log(entry.contentRect);
      setHeight(entry.contentRect.height);
    })
    ro.observe(ref.current!);
    return () => ro.disconnect();
  }, []);
  return (
    <>
      <div
        ref={ref}
        style={{ backgroundColor: "red", width: "200px", height: "200px" }}
      />
      <div
        style={{
          backgroundColor: "blue",
          width: "200px",
          height: `${height}px`
        }}
      />
    </>
  );
}

Doesn't measure position updates when size doesn't change

I'm not sure if the issue title above is 100% correct, but I think it's the best way to describe my issue.

See this code sandbox and notice that the rightX variable doesn't update properly when the div's min-width is met (and the bottom scroller appears):

use-measure-wrong

It only updates to the correct position after the div is scrolled. On the other hand, the wrapper div position is correct until the div is scrolled.

useMeasure should report the correct position of the child div (the one with the scrollbar) when the parent moves.

Get position with default scroll values

I use useMeasure on a simple div element. When I refresh the page, the scroll stays at the same position after the refresh as before, both on Safari, Chrome and Firefox.
My element's position values change because it doesn't calculate the window scroll values to add the Y overflow to the top/Y and the X overflow to the left/X.
I know this is the way getBoundingClientRect works but it could be very useful to get the right position values.

I've made a small repro here and a video:
https://user-images.githubusercontent.com/14930937/145437063-1438c794-d2af-4a02-a21a-7e241c7f37d7.mov

The scroll option fix this problem but it re-render the component each time I scroll, which is not fully efficient.
Maybe it could be a good solution to pass an option for this specific behavior?

debounce should not affect first measure

I want to set debounce for resizing but it is currently affecting the first measurement of the component, which is slowing down render of child components that requires the measurements.

Get left element position after react-spring animation

Hi everyone!

First at all, congratulation for your job with react-spring and react-use-measure: I am starting to use it and it's awesome the things you can do with simple code.

The issue I found is after trying to get the left position of an element.

I tried to get it from an animated element (via react-spring). I noticed that useMeasure hook is retuning the bounds object with the left position equals to the start of the animation.

Is there any way to get the position from useMeasure after the animation is completed?

Thanks in advance.

Incorrect measurements after Next.js soft navigation on mobile

I'll add more details when I have the chance, but incase anyone stumbles across this in the meantime: I was using measure in my Next.js 13.4.x site with app router and for some reason, the widths returned from useMeasure() were incorrect after soft navigation. They were correct on initial load of the page. Then I navigated to another page and navigated back and they were around 8px (they should've been 290px). This only happened on mobile.

As a temporary fix I added a custom use measure hook without any debouncing:

import { useState, useRef, useLayoutEffect, RefObject } from "react";

type Rect = {
  x: number;
  y: number;
  width: number;
  height: number;
};

type UseMeasureReturnType = [RefObject<HTMLElement>, Rect];

export function useMeasure(): UseMeasureReturnType {
  const [rect, setRect] = useState<Rect>({ x: 0, y: 0, width: 0, height: 0 });

  const ref = useRef<HTMLElement | null>(null);

  useLayoutEffect(() => {
    let timeoutId: ReturnType<typeof setTimeout> | null = null;

    function handleResize() {
      if (timeoutId !== null) {
        clearTimeout(timeoutId);
      }

      timeoutId = setTimeout(() => {
        if (ref.current !== null) {
          setRect(ref.current.getBoundingClientRect());
        }
        /* todo – ideally we could debounce this, but that breaks during soft nav */
      }, 0);
    }

    handleResize();

    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return [ref, rect];
}

And that seems to do the trick, but there's likely something weirder going on here.

New reference of ref everytime useMeasure is called.

// the ref we expose to the user const ref = (node: HTMLOrSVGElement | null) => { if (!node || node === state.current.element) return removeListeners() state.current.element = node state.current.scrollContainers = findScrollContainers(node) addListeners() }

Why should this functional ref, not be memoized using useCallback?
If a parent component is using useMeasure and drilling down this ref to it's children, it causes unnecessary re-renders.

bug: react 18 out of sync state updates

hi there. just wanted to flag a little bug with setting state from ResizeObserver callbacks that happens in React 18 due to setState batching. we are seeing this issue surface in react-use-measure currently. the solution is to wrap the internal setState influshSync from ReactDOM:

facebook/react#24331

there may be performance ramifications in doing this, so it might be a good thing to have as opt-in

Use custom debounce method

Is there a way to use a custom debounce method? My project is using lodash/debounce and this package installs its own debounce dependency. In order to save bytes, can I use lodash/debounce with this package? I'd like a similar API to adding a ResizeObserver polyfill.

const [ref, bounds] = useMeasure({ debounceFn: customDebounce });

Thank you for this package!

[BUG] not works with React.forwardRef

not works with React.forwardRef

const Button = React.forwardRef(function Button({ children }, ref) {
  return <button ref={ ref }>{ children }</button>
})


function Component() {

  const [buttonRef, buttonSize] = useMeasure()

  console.log(buttonSize) // always return {top: 0, width: 0, ...}
  // only if i resize window, buttonSize is changes
  
  return <Button ref={buttonRef}>my button</Button>
}

Leave ResizeObserver polyfilling to userland

Currently the ResizeObserver polyfill takes up 80% of the entire package size - https://bundlephobia.com/[email protected].

I think it'd be good to leave the polyfilling to the library users (with recommendation) as there are a few options that each comes with pro and cons.

Some devs prefer polyfilling on-demand using services like http://polyfill.io/ and https://github.com/wessberg/polyfiller that only load the polyfill for platforms that require it.

snowpack failed to compile

✖ snowpack failed to load node_modules/react-use-measure/dist/web.js
  'debounce' is not exported by node_modules/debounce/index.js, imported by node_modules/react-use-measure/dist/web.js

Does not work on Internet Explorer 11

I get a SCRIPT1010 error and it crashes my app when I open it in IE11.

It points to the useMeasure() function declaration in my main.js, when i comment out the useMeasure() hook in my react code, the error goes away.

Feature request: optionally apply CSS transforms.

ResizeObserver reports the size before CSS transforms are applied, but sometimes that's not what we want. e.g. tldraw heavily relies on CSS transforms, and sometimes we need to measure the "apparent" size of an element after it has been zoomed in.

One way of doing this would be getComputedStyle(el).getPropertyValue(‘transform’)

The width doesn't change properly with flexbox + svgs

If you see it here:

https://codesandbox.io/s/stupefied-lehmann-hncx1?file=/src/App.js

I've created an example of this issue. Basically it works on the first render, but then, if you resize it, it basically breaks, the width doesn't get updated.

But interestingly, as soon as you put something like width - 16 on the svg, it starts to resize properly. And as bigger the number you put there, faster the block will resize.

Does anyone has any idea on what's going ho here?

Screen.Recording.2021-03-31.at.2.36.58.PM.mov

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.