Skip to content

Instantly share code, notes, and snippets.

@morajabi
Created February 18, 2019 14:35
Show Gist options
  • 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()
}
@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