Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions docs/useMeasure.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const Demo = () => {

return (
<div ref={ref}>
<div>x: {x}</div>
<div>x: {x}</div>
<div>y: {y}</div>
<div>width: {width}</div>
<div>height: {height}</div>
Expand All @@ -25,21 +25,40 @@ const Demo = () => {
};
```

This hook uses [`ResizeObserver` API][resize-observer], if you want to support
legacy browsers, consider installing [`resize-observer-polyfill`][resize-observer-polyfill]
before running your app.
You can also track an element you already hold a ref to, by passing it via the
`ref` option &mdash; the hook will observe `ref.current` instead of the element
assigned to the callback ref it returns. This also allows using multiple
`useMeasure` hooks in the same component to track different elements.

```jsx
import { useMeasure } from "react-use";

const Demo = () => {
const myRef = useRef(null);
const { width, height } = useMeasure({ ref: myRef })[1];

return (
<div ref={myRef}>
<div>width: {width}</div>
<div>height: {height}</div>
</div>
);
};
```

This hook uses [`ResizeObserver` API][resize-observer], if you want to support
legacy browsers, consider installing [`resize-observer-polyfill`][resize-observer-polyfill]
before running your app.

```js
if (!window.ResizeObserver) {
window.ResizeObserver = (await import('resize-observer-polyfill')).default;
window.ResizeObserver = (await import("resize-observer-polyfill")).default;
}
```


## Related hooks

- [useSize](./useSize.md)


[resize-observer]: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver
[resize-observer-polyfill]: https://www.npmjs.com/package/resize-observer-polyfill
22 changes: 17 additions & 5 deletions src/useMeasure.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { RefObject, useMemo, useState } from 'react';
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
import { isBrowser, noop } from './misc/util';

Expand All @@ -8,6 +8,14 @@ export type UseMeasureRect = Pick<
>;
export type UseMeasureRef<E extends Element = Element> = (element: E) => void;
export type UseMeasureResult<E extends Element = Element> = [UseMeasureRef<E>, UseMeasureRect];
export interface UseMeasureOptions<E extends Element = Element> {
/**
* An optional external ref of the element to measure. When provided, the
* hook observes `ref.current` instead of the element assigned to the
* callback ref it returns.
*/
ref?: RefObject<E>;
}

const defaultState: UseMeasureRect = {
x: 0,
Expand All @@ -20,9 +28,12 @@ const defaultState: UseMeasureRect = {
right: 0,
};

function useMeasure<E extends Element = Element>(): UseMeasureResult<E> {
function useMeasure<E extends Element = Element>(
options?: UseMeasureOptions<E>
): UseMeasureResult<E> {
const [element, ref] = useState<E | null>(null);
const [rect, setRect] = useState<UseMeasureRect>(defaultState);
const externalRef = options ? options.ref : undefined;

const observer = useMemo(
() =>
Expand All @@ -36,12 +47,13 @@ function useMeasure<E extends Element = Element>(): UseMeasureResult<E> {
);

useIsomorphicLayoutEffect(() => {
if (!element) return;
observer.observe(element);
const target = externalRef ? externalRef.current : element;
if (!target) return;
observer.observe(target);
return () => {
observer.disconnect();
};
}, [element]);
}, [element, externalRef, externalRef && externalRef.current]);

return [ref, rect];
}
Expand Down
76 changes: 76 additions & 0 deletions tests/useMeasure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,79 @@ it('calls .disconnect() on ResizeObserver when component unmounts', () => {

expect(disconnect).toHaveBeenCalledTimes(1);
});

it('tracks rectangle of an element passed via options.ref', () => {
let listener: ((rect: any) => void) | undefined = undefined;
const observed: Element[] = [];
(window as any).ResizeObserver = class ResizeObserver {
constructor(ls) {
listener = ls;
}
observe(el: Element) {
observed.push(el);
}
disconnect() {}
};

const div = document.createElement('div');
const externalRef = { current: div as Element | null };

const { result } = renderHook(() => useMeasure({ ref: externalRef }));

expect(observed).toContain(div);
expect(result.current[1]).toMatchObject({ width: 0, height: 0 });

act(() => {
listener!([
{
contentRect: {
x: 3,
y: 4,
width: 300,
height: 150,
top: 30,
bottom: 40,
left: 50,
right: 60,
},
},
]);
});

expect(result.current[1]).toMatchObject({
x: 3,
y: 4,
width: 300,
height: 150,
top: 30,
bottom: 40,
left: 50,
right: 60,
});
});

it('observes a new element when options.ref.current changes', () => {
const observed: Element[] = [];
(window as any).ResizeObserver = class ResizeObserver {
constructor() {}
observe(el: Element) {
observed.push(el);
}
disconnect() {}
};

const divA = document.createElement('div');
const divB = document.createElement('div');
const externalRef = { current: divA as Element | null };

const { rerender } = renderHook(({ ref }) => useMeasure({ ref }), {
initialProps: { ref: externalRef },
});

expect(observed).toEqual([divA]);

externalRef.current = divB;
rerender({ ref: externalRef });

expect(observed).toEqual([divA, divB]);
});