Skip to content

Instantly share code, notes, and snippets.

@morajabi
Created February 18, 2019 14:35
Show Gist options
  • Star 57 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save morajabi/523d7a642d8c0a2f71fcfa0d8b3d2846 to your computer and use it in GitHub Desktop.
Save morajabi/523d7a642d8c0a2f71fcfa0d8b3d2846 to your computer and use it in GitHub Desktop.
useRect — getBoundingClientRect() React Hook with resize handler
import { useLayoutEffect, useCallback, useState } from 'react'
export const useRect = (ref) => {
const [rect, setRect] = useState(getRect(ref ? ref.current : null))
const handleResize = useCallback(() => {
if (!ref.current) {
return
}
// Update client rect
setRect(getRect(ref.current))
}, [ref])
useLayoutEffect(() => {
const element = ref.current
if (!element) {
return
}
handleResize()
if (typeof ResizeObserver === 'function') {
let resizeObserver = new ResizeObserver(() => handleResize())
resizeObserver.observe(element)
return () => {
if (!resizeObserver) {
return
}
resizeObserver.disconnect()
resizeObserver = null
}
} else {
// Browser support, remove freely
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}
}, [ref.current])
return rect
}
function getRect(element) {
if (!element) {
return {
bottom: 0,
height: 0,
left: 0,
right: 0,
top: 0,
width: 0,
}
}
return element.getBoundingClientRect()
}
@intelliapps-io
Copy link

intelliapps-io commented Jul 26, 2019

I made some changes so it can be used with TypeScript. https://codesandbox.io/s/userect-hook-1y5t7

@vincerubinetti
Copy link

vincerubinetti commented Nov 15, 2019

Here's an alternative that I came up with that seems to work:

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

export const useBbox = () => {
  const ref = useRef();
  const [bbox, setBbox] = useState({});

  const set = () =>
    setBbox(ref && ref.current ? ref.current.getBoundingClientRect() : {});

  useEffect(() => {
    set();
    window.addEventListener('resize', set);
    return () => window.removeEventListener('resize', set);
  }, []);

  return [bbox, ref];
};

Then to use it:

const SignIn = () => {
  const [bbox, ref] = useBbox();

  return (
    <>
      <Button ref={ref}>open popup</Button>
      <Popup anchorBbox={bbox}>popup content</Popup>
    </>
  );
};

This is modeled after this example in the React docs, but is modified to also update the bbox any time the window is resized, which may or may not be enough for your use case; use this with caution.

EDIT YEARS LATER: See comments below which add more listeners for things that could affect the bounding box. Scrolling will affect bbox, but also any kind of document reflow could too, so you could add a MutationObserver listener or something like that. So again, be careful using this. In my case, if I recall, I was just using the width/height parts of the bbox, so tracking scroll didn't really matter.


Related:
facebook/react#15176

Also here's an example of a library using hooks to update positions of a popup, but with added debouncing and other stuff:
https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/Popover/Popover.js

@mdo5004
Copy link

mdo5004 commented May 19, 2020

Using this hook, I receive the following warning:

React Hook useLayoutEffect has missing dependencies: 'handleResize' and 'ref'. Either include them or remove the dependency array. Mutable values like 'ref.current' aren't valid dependencies because mutating them doesn't re-render the component  react-hooks/exhaustive-deps

Accordingly, the dependency array for useLayoutEffect should be [ref, handleResize], not [ref.current].

@charleshimmer
Copy link

@intelliapps-io no need to define an interface of RectResult. Just use the type the browser returns from element.getBoundingClientRect() of DOMRef.

@mantagen
Copy link

mantagen commented Oct 6, 2020

Possibly worth mentioning that this function hook really be listening for a 'scroll' event too, since top, right, bottom, left are relative to the viewport, not absolute!

@charleshimmer
Copy link

@mantagen it's not listening for scroll event, it's listening for a resize event.

@mantagen
Copy link

mantagen commented Oct 7, 2020

Whoops, thanks @charleshimmer -- I mean to say that this function hook should really be listening for a 'scroll' event too.

In it's current form, if a user scrolls, the hook will return potentially stale values for top, right, bottom, and left.

@thediveo
Copy link

@mantagen it depends on what you want/need to achieve. If you "only" want to position other elements within the same reference, then scrolling doesn't matter (if I'm not mistaken).

@thediveo
Copy link

I'm wondering if it makes an important difference of using useEffect instead of useLayoutEffect: for some reasons I seem to not always get the final bounding rect on some elements out of a larger offset, but only almost. That I would suspect to be a sign that there is still some lay-outing going on after the time useLayoutEffect triggered. Can someone please enlighten me, as I'm under the impression that I'm lacking understanding of the bigger picture.

@pdevito3
Copy link

Here's an alternative that I came up with that seems to work:

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

export const useBbox = () => {
  const ref = useRef();
  const [bbox, setBbox] = useState({});

  const set = () =>
    setBbox(ref && ref.current ? ref.current.getBoundingClientRect() : {});

  useEffect(() => {
    set();
    window.addEventListener('resize', set);
    return () => window.removeEventListener('resize', set);
  }, []);

  return [bbox, ref];
};

Then to use it:

const SignIn = () => {
  const [bbox, ref] = useBbox();

  return (
    <>
      <Button ref={ref}>open popup</Button>
      <Popup anchorBbox={bbox}>popup content</Popup>
    </>
  );
};

This is modeled after this example in the React docs, but is modified to also update the bbox any time the window is resized (which may or may not be enough for your use case; use this with caution).

It seems to unmount the listener properly, but there could definitely be something I missed. Comments are welcome.

Related:
facebook/react#15176

Also here's an example of a library using hooks to update positions of a popup, but with added debouncing and other stuff:
https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/Popover/Popover.js

this was a great base for what i needed. cheers!

@fraangon
Copy link

Whoops, thanks @charleshimmer -- I mean to say that this function hook should really be listening for a 'scroll' event too.

In it's current form, if a user scrolls, the hook will return potentially stale values for top, right, bottom, and left.

I modify the hook to fix this problem

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

export const useRect = () => {
  const ref = useRef();
  const [rect, setRect] = useState({});

  const set = () => setRect(ref && ref.current ? ref.current.getBoundingClientRect() : {});

  const useEffectInEvent = (event, useCapture) => {
    useEffect(() => {
      set();
      window.addEventListener(event, set, useCapture);
      return () => window.removeEventListener(event, set, useCapture);
    }, []);
  };

  useEffectInEvent('resize');
  useEffectInEvent('scroll', true);

  return [rect, ref];
};

@josefrichter
Copy link

@francogonzalezorellano it seems like you should rather be passing the ref to this, no? Something like

export const useRect = (ref) => {
  // const ref = useRef();
...
};

or am I missing something?

@fraangon
Copy link

fraangon commented Oct 6, 2021

@francogonzalezorellano it seems like you should rather be passing the ref to this, no? Something like

export const useRect = (ref) => {
  // const ref = useRef();
...
};

or am I missing something?

@josefrichter Yes, you can define the ref outside the hook and use it like that.

I prefer to define it inside and return it, then I can use it like this:

const [rect, ref] = useRect();

@josefrichter
Copy link

@francogonzalezorellano it seems like you should rather be passing the ref to this, no? Something like

export const useRect = (ref) => {
  // const ref = useRef();
...
};

or am I missing something?

@josefrichter Yes, you can define the ref outside the hook and use it like that.

I prefer to define it inside and return it, then I can use it like this:

const [rect, ref] = useRect();

aah now I understand how you meant it. yeah that's better! thanks

@va3093
Copy link

va3093 commented Oct 13, 2021

Whoops, thanks @charleshimmer -- I mean to say that this function hook should really be listening for a 'scroll' event too.
In it's current form, if a user scrolls, the hook will return potentially stale values for top, right, bottom, and left.

I modify the hook to fix this problem

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

export const useRect = () => {
  const ref = useRef();
  const [rect, setRect] = useState({});

  const set = () => setRect(ref && ref.current ? ref.current.getBoundingClientRect() : {});

  const useEffectInEvent = (event, useCapture) => {
    useEffect(() => {
      set();
      window.addEventListener(event, set, useCapture);
      return () => window.removeEventListener(event, set, useCapture);
    }, []);
  };

  useEffectInEvent('resize');
  useEffectInEvent('scroll', true);

  return [rect, ref];
};

I added types for this:

export const useRect = <T extends Element>(): [
  DOMRect | undefined,
  MutableRefObject<T | null>
] => {
  const ref = useRef<T>(null);
  const [rect, setRect] = useState<DOMRect>();

  const set = () => setRect(ref.current?.getBoundingClientRect());

  const useEffectInEvent = (
    event: "resize" | "scroll",
    useCapture?: boolean
  ) => {
    useEffect(() => {
      set();
      window.addEventListener(event, set, useCapture);
      return () => window.removeEventListener(event, set, useCapture);
    }, []);
  };

  useEffectInEvent("resize");
  useEffectInEvent("scroll", true);

  return [rect, ref];

@BenHakimIlyass
Copy link

Quick update (better to declare our hooks outside of the render):

  const useEffectInEvent = (event: "resize" | "scroll", useCapture?: boolean, set?: () => void ) => {
    useEffect(() => {
      set();
      window.addEventListener(event, set, useCapture);
      return () => window.removeEventListener(event, set, useCapture);
    }, []);
  };

export const useRect = <T extends Element>(): [
  DOMRect | undefined,
  MutableRefObject<T | null>
] => {
  const ref = useRef<T>(null);
  const [rect, setRect] = useState<DOMRect>();

  const set = () => setRect(ref.current?.getBoundingClientRect());

  useEffectInEvent("resize", set);
  useEffectInEvent("scroll", true, set);

  return [rect, ref];

@raarts
Copy link

raarts commented Apr 15, 2022

But now useEffectInEvent() can't find the set() function..

@chidimo
Copy link

chidimo commented Nov 3, 2022

Quick update (better to declare our hooks outside of the render):

  const useEffectInEvent = (event: "resize" | "scroll", useCapture?: boolean, set?: () => void ) => {
    useEffect(() => {
      set();
      window.addEventListener(event, set, useCapture);
      return () => window.removeEventListener(event, set, useCapture);
    }, []);
  };

export const useRect = <T extends Element>(): [
  DOMRect | undefined,
  MutableRefObject<T | null>
] => {
  const ref = useRef<T>(null);
  const [rect, setRect] = useState<DOMRect>();

  const set = () => setRect(ref.current?.getBoundingClientRect());

  useEffectInEvent("resize", set);
  useEffectInEvent("scroll", true, set);

  return [rect, ref];

This works okay so far. Thank you

@DIEGOHORVATTI
Copy link

DIEGOHORVATTI commented Aug 4, 2023

@zlwu
Copy link

zlwu commented Sep 11, 2023

Make useEffectInEvent hook more generic for more events, useRect for other element type.

import { useState, useRef, useEffect } from "react";

type MutableRefObject<T> = {
  current: T;
};

export const useEffectInEvent = <K extends keyof WindowEventMap>(
  event: K,
  set: () => void,
  useCapture?: boolean,
) => {
  useEffect(() => {
    if (set) {
      set();
      window.addEventListener(event, set, useCapture);

      return () => window.removeEventListener(event, set, useCapture);
    }

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
};

export const useRect = <T extends HTMLElement | null>(): [
  DOMRect | undefined,
  MutableRefObject<T | null>,
] => {
  const ref = useRef<T>(null);
  const [rect, setRect] = useState<DOMRect>();

  const set = (): void => {
    setRect(ref.current?.getBoundingClientRect());
  };

  useEffectInEvent("resize", set);
  useEffectInEvent("scroll", set, true);

  return [rect, ref];
};

@DIEGOHORVATTI
Copy link

DIEGOHORVATTI commented Sep 11, 2023

What I've been using since then to fetch the screen size and the choice of 'resize' or 'scroll' is by prop to avoid having two events with one not being used.

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

type MutableRefObject<T> = {
  current: T
}

type EventType = 'resize' | 'scroll'

const useEffectInEvent = (
  event: EventType,
  useCapture?: boolean,
  set?: () => void
) => {
  useEffect(() => {
    if (set) {
      set()
      window.addEventListener(event, set, useCapture)

      return () => window.removeEventListener(event, set, useCapture)
    }
  }, [])
}

export const useRect = <T extends HTMLDivElement | null>(
  event: EventType = 'resize'
): [DOMRect | undefined, MutableRefObject<T | null>, number] => {
  const [rect, setRect] = useState<DOMRect>()

  const reference = useRef<T>(null)

  const [screenHeight, setScreenHeight] = useState(window.innerHeight)

  const set = (): void => {
    setRect(reference.current?.getBoundingClientRect())
  }

  useEffectInEvent(event, true, set)
  const handleResize = () => {
    setScreenHeight(window.innerHeight)
  }

  useEffect(() => {
    window.addEventListener(event, handleResize)
    return () => {
      window.removeEventListener(event, handleResize)
    }
  }, [])

  return [rect, reference, screenHeight]
}
   const [rect, reference, screenHeight] = useRect('resize')

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment