-
-
Save morajabi/523d7a642d8c0a2f71fcfa0d8b3d2846 to your computer and use it in GitHub Desktop.
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() | |
} |
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
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]
.
@intelliapps-io no need to define an interface of RectResult. Just use the type the browser returns from element.getBoundingClientRect()
of DOMRef.
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!
@mantagen it's not listening for scroll event, it's listening for a resize event.
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
.
@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).
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.
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#15176Also 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!
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
, andleft
.
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];
};
@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?
@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();
@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
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 fortop
,right
,bottom
, andleft
.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];
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];
But now useEffectInEvent() can't find the set() function..
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
wow guys, let's study a clear code
[https://codesandbox.io/s/userect-hook-forked-t39z5h?file=/src/index.tsx)
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];
};
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')
I made some changes so it can be used with TypeScript. https://codesandbox.io/s/userect-hook-1y5t7