Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(useMeasure): react suspense error #2506

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 16 additions & 19 deletions src/useMeasure.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { useMemo, useState } from 'react';
import { useRef, useState, MutableRefObject } from 'react';
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
import { isBrowser, noop } from './misc/util';
import { isBrowser } from './misc/util';

export type UseMeasureRect = Pick<
DOMRectReadOnly,
'x' | 'y' | 'top' | 'left' | 'right' | 'bottom' | 'height' | 'width'
>;
export type UseMeasureRef<E extends Element = Element> = (element: E) => void;
export type UseMeasureRef<E extends Element = Element> = MutableRefObject<E | null>;
export type UseMeasureResult<E extends Element = Element> = [UseMeasureRef<E>, UseMeasureRect];

const defaultRef = { current: null };
const defaultState: UseMeasureRect = {
x: 0,
y: 0,
Expand All @@ -21,31 +22,27 @@ const defaultState: UseMeasureRect = {
};

function useMeasure<E extends Element = Element>(): UseMeasureResult<E> {
const [element, ref] = useState<E | null>(null);
const ref = useRef<E | null>(null);
const [rect, setRect] = useState<UseMeasureRect>(defaultState);

const observer = useMemo(
() =>
new (window as any).ResizeObserver((entries) => {
if (entries[0]) {
const { x, y, width, height, top, left, bottom, right } = entries[0].contentRect;
setRect({ x, y, width, height, top, left, bottom, right });
}
}),
[]
);

useIsomorphicLayoutEffect(() => {
if (!element) return;
observer.observe(element);
if (!ref.current) return;

const observer = new (window as any).ResizeObserver((entries) => {
if (entries[0]) {
const { x, y, width, height, top, left, bottom, right } = entries[0].contentRect;
setRect({ x, y, width, height, top, left, bottom, right });
}
});
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, [element]);
}, []);

return [ref, rect];
}

export default isBrowser && typeof (window as any).ResizeObserver !== 'undefined'
? useMeasure
: ((() => [noop, defaultState]) as typeof useMeasure);
: ((() => [defaultRef, defaultState]) as typeof useMeasure);
154 changes: 78 additions & 76 deletions tests/useMeasure.test.ts → tests/useMeasure.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { renderHook, act } from '@testing-library/react-hooks';
import useMeasure, { UseMeasureRef } from '../src/useMeasure';
import { render } from '@testing-library/react';

import useMeasure from '../src/useMeasure';
import React, { useEffect } from 'react';

const TestComponent = () => {
const [ref] = useMeasure<HTMLDivElement>();
return <div ref={ref} />;
};

it('by default, state defaults every value to -1', () => {
const { result } = renderHook(() => useMeasure());

act(() => {
const div = document.createElement('div');
(result.current[0] as UseMeasureRef)(div);
result.current[0].current = div;
});

expect(result.current[1]).toMatchObject({
Expand All @@ -29,12 +37,7 @@ it('synchronously sets up ResizeObserver listener', () => {
disconnect() {}
};

const { result } = renderHook(() => useMeasure());

act(() => {
const div = document.createElement('div');
(result.current[0] as UseMeasureRef)(div);
});
render(<TestComponent />);

expect(typeof listener).toBe('function');
});
Expand All @@ -49,31 +52,34 @@ it('tracks rectangle of a DOM element', () => {
disconnect() {}
};

const { result } = renderHook(() => useMeasure());

act(() => {
const div = document.createElement('div');
(result.current[0] as UseMeasureRef)(div);
});
let currentMeasure: any = null;
const TestComponent = () => {
const [ref, measure] = useMeasure<HTMLDivElement>();
useEffect(() => {
currentMeasure = measure;
}, [measure]);
return <div ref={ref} />;
};

act(() => {
listener!([
{
contentRect: {
x: 1,
y: 2,
width: 200,
height: 200,
top: 100,
bottom: 0,
left: 100,
right: 0,
},
const rendered = render(<TestComponent />);

listener!([
{
contentRect: {
x: 1,
y: 2,
width: 200,
height: 200,
top: 100,
bottom: 0,
left: 100,
right: 0,
},
]);
});
},
]);
rendered.rerender(<TestComponent />);

expect(result.current[1]).toMatchObject({
expect(currentMeasure).toMatchObject({
x: 1,
y: 2,
width: 200,
Expand All @@ -95,31 +101,33 @@ it('tracks multiple updates', () => {
disconnect() {}
};

const { result } = renderHook(() => useMeasure());

act(() => {
const div = document.createElement('div');
(result.current[0] as UseMeasureRef)(div);
});

act(() => {
listener!([
{
contentRect: {
x: 1,
y: 1,
width: 1,
height: 1,
top: 1,
bottom: 1,
left: 1,
right: 1,
},
let currentMeasure: any = null;
const TestComponent = () => {
const [ref, measure] = useMeasure<HTMLDivElement>();
useEffect(() => {
currentMeasure = measure;
}, [measure]);
return <div ref={ref} />;
};
const rendered = render(<TestComponent />);

listener!([
{
contentRect: {
x: 1,
y: 1,
width: 1,
height: 1,
top: 1,
bottom: 1,
left: 1,
right: 1,
},
]);
});
},
]);
rendered.rerender(<TestComponent />);

expect(result.current[1]).toMatchObject({
expect(currentMeasure).toMatchObject({
x: 1,
y: 1,
width: 1,
Expand All @@ -130,24 +138,23 @@ it('tracks multiple updates', () => {
right: 1,
});

act(() => {
listener!([
{
contentRect: {
x: 2,
y: 2,
width: 2,
height: 2,
top: 2,
bottom: 2,
left: 2,
right: 2,
},
listener!([
{
contentRect: {
x: 2,
y: 2,
width: 2,
height: 2,
top: 2,
bottom: 2,
left: 2,
right: 2,
},
]);
});
},
]);
rendered.rerender(<TestComponent />);

expect(result.current[1]).toMatchObject({
expect(currentMeasure).toMatchObject({
x: 2,
y: 2,
width: 2,
Expand All @@ -168,16 +175,11 @@ it('calls .disconnect() on ResizeObserver when component unmounts', () => {
}
};

const { result, unmount } = renderHook(() => useMeasure());

act(() => {
const div = document.createElement('div');
(result.current[0] as UseMeasureRef)(div);
});
const rendered = render(<TestComponent />);

expect(disconnect).toHaveBeenCalledTimes(0);

unmount();
rendered.unmount();

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