Created
February 18, 2019 14:35
useRect — getBoundingClientRect() React Hook with resize handler
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} |
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')
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I added types for this: