diff --git a/docs/useMeasure.md b/docs/useMeasure.md
index 306fec3b6a..5b70979e6b 100644
--- a/docs/useMeasure.md
+++ b/docs/useMeasure.md
@@ -12,7 +12,7 @@ const Demo = () => {
return (
-
x: {x}
+
x: {x}
y: {y}
width: {width}
height: {height}
@@ -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 — 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 (
+
+
width: {width}
+
height: {height}
+
+ );
+};
+```
+
+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
diff --git a/src/useMeasure.ts b/src/useMeasure.ts
index e14f217f1c..44dd74ad31 100644
--- a/src/useMeasure.ts
+++ b/src/useMeasure.ts
@@ -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';
@@ -8,6 +8,14 @@ export type UseMeasureRect = Pick<
>;
export type UseMeasureRef
= (element: E) => void;
export type UseMeasureResult = [UseMeasureRef, UseMeasureRect];
+export interface UseMeasureOptions {
+ /**
+ * 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;
+}
const defaultState: UseMeasureRect = {
x: 0,
@@ -20,9 +28,12 @@ const defaultState: UseMeasureRect = {
right: 0,
};
-function useMeasure(): UseMeasureResult {
+function useMeasure(
+ options?: UseMeasureOptions
+): UseMeasureResult {
const [element, ref] = useState(null);
const [rect, setRect] = useState(defaultState);
+ const externalRef = options ? options.ref : undefined;
const observer = useMemo(
() =>
@@ -36,12 +47,13 @@ function useMeasure(): UseMeasureResult {
);
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];
}
diff --git a/tests/useMeasure.test.ts b/tests/useMeasure.test.ts
index c32fdaeee7..1acee26a6a 100644
--- a/tests/useMeasure.test.ts
+++ b/tests/useMeasure.test.ts
@@ -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]);
+});