usehooks-ts icon indicating copy to clipboard operation
usehooks-ts copied to clipboard

useElementSize ref not passable to other hooks

Open philipgher opened this issue 2 years ago • 1 comments

It is currently not possible to easily pass the ref exposed from useElementSize to other hooks.

As the useElementSize hook internally stores the ref in a state while exposing the ref via the type (node: T | null) => void it is not a "normal" RefObject thus not useable by passing it to other hooks.

My usecase is having 1 ref that is attached to a scrollable container. I'd like to monotor this elements size as well as attaching the scroll event handler.

const [scrollableRef, { height: scrollableElementHeight }] = useElementSize<HTMLUListElement>();

const handleScroll = () => { ... }

useEventListener('scroll', handleScrollList, scrollableRef);

This causes the scroll event handler to never fire while triggering a Typescript error.

No overload matches this call.

It'd be nice if the useElementSize exposes a value that can be passed around to other hooks and places as it were a normal RefObject.

philipgher avatar Aug 26 '22 09:08 philipgher

I've hacked up a workaround by exposing the ref as a normal RefObject<T> being a third element in the return array.

Resulting in the type:

function useElementSize<T extends HTMLElement = HTMLDivElement>(): [(node: T | null) => void, Size, RefObject<T>]

This is achieved by doing the following:

import { RefObject, useCallback, useState } from 'react';

// See: https://usehooks-ts.com/react-hook/use-event-listener
import useEventListener from './useEventListener';
// See: https://usehooks-ts.com/react-hook/use-isomorphic-layout-effect
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';

interface Size {
    width: number;
    height: number;
}

function useElementSize<T extends HTMLElement = HTMLDivElement>(): [
    (node: T | null) => void,
    Size,
    RefObject<T>
] {
    // Mutable values like 'ref.current' aren't valid dependencies
    // because mutating them doesn't re-render the component.
    // Instead, we use a state as a ref to be reactive.
    const [ref, setRef] = useState<T | null>(null);
    const [size, setSize] = useState<Size>({
        width: 0,
        height: 0
    });

    // Prevent too many rendering using useCallback
    const handleSize = useCallback(() => {
        setSize({
            width: ref?.offsetWidth || 0,
            height: ref?.offsetHeight || 0
        });

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [ref?.offsetHeight, ref?.offsetWidth]);

    useEventListener('resize', handleSize);

    useIsomorphicLayoutEffect(() => {
        handleSize();
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [ref?.offsetHeight, ref?.offsetWidth]);

    return [setRef, size, { current: ref }];
}

export default useElementSize;

In the component this usage looks like this:

const [scrollableRef, { width, height }, scrollableRefInstance] = useElementSize<HTMLUListElement>();

...

<Component ref={scrollableRef}/>

Although the workaround does work, I am personally not a huge fan of the double ref in the returned array.

Maybe someone has a better idea of implementing the useElementSize in such a way that 1 single ref is exposed while maintaining limited rerenders when resizing and so forth. - while having it be of type RefObject obviously.

philipgher avatar Aug 26 '22 09:08 philipgher

Hi, useElementSize has been deprecated and should be replaced by useResizeObserver.

juliencrn avatar Feb 06 '24 14:02 juliencrn