-
Notifications
You must be signed in to change notification settings - Fork 99
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
214 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { RefObject, useEffect } from 'react'; | ||
import { useSyncedRef } from './useSyncedRef'; | ||
|
||
export type IUseResizeObserverCallback = (entry: ResizeObserverEntry) => void; | ||
|
||
interface IResizeObserverSingleton { | ||
observer: ResizeObserver; | ||
subscribe: (target: Element, callback: IUseResizeObserverCallback) => void; | ||
unsubscribe: (target: Element, callback: IUseResizeObserverCallback) => void; | ||
} | ||
|
||
let observerSingleton: IResizeObserverSingleton; | ||
|
||
function getResizeObserver(): IResizeObserverSingleton { | ||
if (observerSingleton) return observerSingleton; | ||
|
||
const callbacks = new Map<Element, Set<IUseResizeObserverCallback>>(); | ||
|
||
const observer = new ResizeObserver((entries) => { | ||
entries.forEach((entry) => callbacks.get(entry.target)?.forEach((cb) => cb(entry))); | ||
}); | ||
|
||
observerSingleton = { | ||
observer, | ||
subscribe: (target, callback) => { | ||
let cbs = callbacks.get(target); | ||
|
||
if (!cbs) { | ||
// if target has no observers yet - register it | ||
cbs = new Set<IUseResizeObserverCallback>(); | ||
callbacks.set(target, cbs); | ||
observer.observe(target); | ||
} | ||
|
||
// as Set is duplicate-safe - simply add callback on each call | ||
cbs.add(callback); | ||
|
||
observer.observe(target); | ||
}, | ||
unsubscribe: (target, callback) => { | ||
const cbs = callbacks.get(target); | ||
|
||
if (cbs) { | ||
// remove current observer | ||
cbs.delete(callback); | ||
|
||
if (!cbs.size) { | ||
// if no observers left unregister target completely | ||
callbacks.delete(target); | ||
observer.unobserve(target); | ||
} | ||
} | ||
}, | ||
}; | ||
|
||
return observerSingleton; | ||
} | ||
|
||
/** | ||
* Invokes a callback whenever ResizeObserver detects a change to target's size. | ||
* | ||
* @param target React reference or Element to track. | ||
* @param callback Callback that will be invoked on resize. | ||
*/ | ||
export function useResizeObserver<T extends Element>( | ||
target: RefObject<T> | T | null, | ||
callback: IUseResizeObserverCallback | ||
): void { | ||
const ro = getResizeObserver(); | ||
const cb = useSyncedRef(callback); | ||
|
||
useEffect(() => { | ||
// 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) => { | ||
if (subscribed) cb.current(...args); | ||
}; | ||
|
||
ro.subscribe(tgt, handler); | ||
|
||
// eslint-disable-next-line consistent-return | ||
return () => { | ||
subscribed = false; | ||
ro.unsubscribe(tgt, handler); | ||
}; | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [target, ro]); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import * as React from 'react'; | ||
import { useRef, useState } from 'react'; | ||
import { IUseResizeObserverCallback, useDebounceCallback, useResizeObserver } from '../../src'; | ||
|
||
export const Example: React.FC = () => { | ||
const ref = useRef<HTMLDivElement | null>(null); | ||
const [rect, setRect] = useState<DOMRectReadOnly>(); | ||
useResizeObserver(ref, (e) => setRect(e.contentRect)); | ||
|
||
return ( | ||
<div> | ||
<pre>{JSON.stringify(rect)}</pre> | ||
<div | ||
ref={ref} | ||
style={{ | ||
minWidth: 100, | ||
minHeight: 100, | ||
resize: 'both', | ||
overflow: 'auto', | ||
background: 'red', | ||
}} | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
export const ExampleDebounced: React.FC = () => { | ||
const ref = useRef<HTMLDivElement | null>(null); | ||
const [rect, setRect] = useState<DOMRectReadOnly>(); | ||
const cb = useDebounceCallback<IUseResizeObserverCallback>((e) => setRect(e.contentRect), 500, [ | ||
setRect, | ||
]); | ||
useResizeObserver(ref, cb); | ||
|
||
return ( | ||
<div> | ||
<pre>{JSON.stringify(rect)}</pre> | ||
<div | ||
ref={ref} | ||
style={{ | ||
minWidth: 100, | ||
minHeight: 100, | ||
resize: 'both', | ||
overflow: 'auto', | ||
background: 'red', | ||
}} | ||
/> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks'; | ||
import { Example, ExampleDebounced } from './useResizeObserver.stories'; | ||
|
||
<Meta title="Sensor/useResizeObserver" /> | ||
|
||
# useResizeObserver | ||
|
||
Invokes a callback whenever ResizeObserver detects a change to target's size. | ||
|
||
- Uses a singe ResizeObserver for all hook instances, as it is more effective than using per-hook | ||
observers. | ||
- No need to wrap callback with `useCallback`, because hook manages callback mutation internally. | ||
- Does not apply any throttle or debounce mechanism - it is on end-developer side. | ||
- Does not produce references for you. | ||
- SSR friendly. | ||
- Provides access to `ResizeObserverEntry` | ||
|
||
#### Example | ||
|
||
<Canvas isColumn> | ||
Below component uses direct invocation so it is not so optimal in terms of CPU usage, but it gains | ||
most recent data. | ||
<Story name="Example" story={Example} /> | ||
As `useResizeObserver` does not apply any debounce or throttle mechanisms to received callback - | ||
it is up to developer to do so if needed passed callback. Below example is almost same as previous | ||
but state is updated within 500ms debounce. | ||
<Story name="ExampleDebounced" story={ExampleDebounced} /> | ||
</Canvas> | ||
|
||
## Reference | ||
|
||
```ts | ||
export function useResizeObserver<T extends Element>( | ||
target: RefObject<T> | T | null, | ||
callback: (entry: ResizeObserverEntry) => void | ||
): void; | ||
``` | ||
|
||
#### Arguments | ||
|
||
- **target** _`RefObject<Element> | Element | null`_ - element to track. | ||
- **callback** _`(entry: ResizeObserverEntry) => void`_ - Callback that will be invoked on resize. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { renderHook } from '@testing-library/react-hooks/dom'; | ||
import { useResizeObserver } from '../../src'; | ||
|
||
describe('useResizeObserver', () => { | ||
it('should be defined', () => { | ||
expect(useResizeObserver).toBeDefined(); | ||
}); | ||
|
||
it('should render', () => { | ||
const { result } = renderHook(() => useResizeObserver(null, () => {})); | ||
|
||
expect(result.error).toBeUndefined(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { renderHook } from '@testing-library/react-hooks/server'; | ||
import { useResizeObserver } from '../../src'; | ||
|
||
describe('useResizeObserver', () => { | ||
it('should be defined', () => { | ||
expect(useResizeObserver).toBeDefined(); | ||
}); | ||
|
||
it('should render', () => { | ||
const { result } = renderHook(() => useResizeObserver(null, () => {})); | ||
|
||
expect(result.error).toBeUndefined(); | ||
}); | ||
}); |