Skip to content
Merged
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
8 changes: 4 additions & 4 deletions src/useMeasure/useMeasure.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RefObject, useRef } from 'react';
import { useSafeState, IUseResizeObserverCallback, useResizeObserver, useRafCallback } from '..';
import { MutableRefObject, useRef } from 'react';
import { IUseResizeObserverCallback, useRafCallback, useResizeObserver, useSafeState } from '..';

/**
* Uses ResizeObserver to track element dimensions and re-render component when they change.
Expand All @@ -8,8 +8,8 @@ import { useSafeState, IUseResizeObserverCallback, useResizeObserver, useRafCall
*/
export function useMeasure<T extends Element>(
enabled = true
): [DOMRectReadOnly | undefined, RefObject<T>] {
const elementRef = useRef<T>(null);
): [DOMRectReadOnly | undefined, MutableRefObject<T | null>] {
const elementRef = useRef<T | null>(null);
const [rect, setRect] = useSafeState<DOMRectReadOnly>();
const [observerHandler] = useRafCallback<IUseResizeObserverCallback>((entry) =>
setRect(entry.contentRect)
Expand Down
33 changes: 33 additions & 0 deletions src/useResizeObserver/__tests__/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,39 @@ describe('useResizeObserver', () => {
expect(ResizeObserverSpy).toHaveBeenCalledTimes(1);
});

it('should subscribe in case ref first was empty but then gained element', () => {
const div = document.createElement('div');
const ref: React.MutableRefObject<Element | null> = { current: null };
const spy = jest.fn();

// eslint-disable-next-line @typescript-eslint/no-shadow
const { rerender } = renderHook(({ ref }) => useResizeObserver(ref, spy), {
initialProps: { ref },
});

expect(observeSpy).toHaveBeenCalledTimes(0);

ref.current = div;
rerender({ ref });

expect(observeSpy).toHaveBeenCalledTimes(1);

const entry = {
target: div,
contentRect: {},
borderBoxSize: {},
contentBoxSize: {},
} as unknown as ResizeObserverEntry;

ResizeObserverSpy.mock.calls[0][0]([entry]);

expect(spy).not.toHaveBeenCalledWith(entry);

jest.advanceTimersByTime(1);

expect(spy).toHaveBeenCalledWith(entry);
});

it('should invoke each callback listening same element asynchronously using setTimeout0', () => {
const div = document.createElement('div');
const spy1 = jest.fn();
Expand Down
15 changes: 10 additions & 5 deletions src/useResizeObserver/useResizeObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,22 @@ export function useResizeObserver<T extends Element>(
const ro = enabled && getResizeObserver();
const cb = useSyncedRef(callback);

const tgt = target && 'current' in target ? target.current : target;

useEffect(() => {
if (!ro) return;
// this secondary target resolve required for case when we receive ref object, which, most
// likely, contains null during render stage, but already populated with element during
// effect stage.
// eslint-disable-next-line @typescript-eslint/no-shadow
const tgt = target && 'current' in target ? target.current : target;

if (!ro || !tgt) return;

// as unsubscription in internals of our ResizeObserver abstraction can
// happen a bit later than effect cleanup invocation - we need a marker,
// that this handler should not be invoked anymore
let subscribed = true;

const tgt = target && 'current' in target ? target.current : target;
if (!tgt) return;

const handler: IUseResizeObserverCallback = (...args) => {
// it is reinsurance for the highly asynchronous invocations, almost
// impossible to achieve in tests, thus excluding from LOC
Expand All @@ -105,5 +110,5 @@ export function useResizeObserver<T extends Element>(
ro.unsubscribe(tgt, handler);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [target, ro]);
}, [tgt, ro]);
}