Skip to content

Commit 537fb18

Browse files
DaniGuardiolajsnajdrDaniGuardiolatyxlaciampo
authored
Add useEvent and useObserveElementSize to @wordpress/compose (#64943)
* Simplify useResizeObserver * Loop through all resize entries * Add `useEvent` util. * Add `useObserveElementSize` util. * Simplify `useResizeObserver` by using `useEvent` and `useObserveElementSize`. * Switch to layout effect and accept refs too. * Prevent initial re-render in ResizeElement. * Better error message. * Improved example of useEvent. * Update packages/compose/src/hooks/use-event/index.ts Co-authored-by: Marin Atanasov <[email protected]> * Sync docs. * Avoid redundant resize listener calls. * Switch to structural check. * Improve example. * Fix docs. * Make `useObserveElementSize` generic. * New API that returns a ref. * Make utility private for now. * Mark legacy `useResizeObserver` as such. * Rename `useObserveElementSize` to `useResizeObserver`. * Add return type. * Add signature as overload. * Add support for legacy API. * Move into subdirectory. * Minor import fix. * Fix docgen to support overloads (will pick up the first function signature). * Replace legacy utility with the new one. * Apply feedback. * Clean up and document. * Added changelog entries. --------- Co-authored-by: jsnajdr <[email protected]> Co-authored-by: DaniGuardiola <[email protected]> Co-authored-by: tyxla <[email protected]> Co-authored-by: ciampo <[email protected]> Co-authored-by: youknowriad <[email protected]>
1 parent 312fa6e commit 537fb18

File tree

9 files changed

+238
-44
lines changed

9 files changed

+238
-44
lines changed

packages/compose/CHANGELOG.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Unreleased
44

5+
### New Features
6+
7+
- `useEvent`: a new utility that creates a stable callback function that has access to the latest state and can be used within event handlers and effect callbacks ([#64943](https://github.com/WordPress/gutenberg/pull/64943)).
8+
- `useResizeObserver`: new and improved version of the utility (legacy API is still supported) ([#64943](https://github.com/WordPress/gutenberg/pull/64943)).
9+
510
## 7.7.0 (2024-09-05)
611

712
## 7.6.0 (2024-08-21)
@@ -205,8 +210,8 @@
205210

206211
### Breaking Changes
207212

208-
- Drop support for Internet Explorer 11 ([#31110](https://github.com/WordPress/gutenberg/pull/31110)). Learn more at https://make.wordpress.org/core/2021/04/22/ie-11-support-phase-out-plan/.
209-
- Increase the minimum Node.js version to v12 matching Long Term Support releases ([#31270](https://github.com/WordPress/gutenberg/pull/31270)). Learn more at https://nodejs.org/en/about/releases/.
213+
- Drop support for Internet Explorer 11 ([#31110](https://github.com/WordPress/gutenberg/pull/31110)). Learn more at <https://make.wordpress.org/core/2021/04/22/ie-11-support-phase-out-plan/>.
214+
- Increase the minimum Node.js version to v12 matching Long Term Support releases ([#31270](https://github.com/WordPress/gutenberg/pull/31270)). Learn more at <https://nodejs.org/en/about/releases/>.
210215

211216
## 3.25.0 (2021-03-17)
212217

packages/compose/README.md

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,29 @@ _Returns_
305305

306306
- `import('react').RefCallback<HTMLElement>`: Element Ref.
307307

308+
### useEvent
309+
310+
Creates a stable callback function that has access to the latest state and can be used within event handlers and effect callbacks. Throws when used in the render phase.
311+
312+
_Usage_
313+
314+
```tsx
315+
function Component( props ) {
316+
const onClick = useEvent( props.onClick );
317+
useEffect( () => {
318+
onClick();
319+
// Won't trigger the effect again when props.onClick is updated.
320+
}, [ onClick ] );
321+
// Won't re-render Button when props.onClick is updated (if `Button` is
322+
// wrapped in `React.memo`).
323+
return <Button onClick={ onClick } />;
324+
}
325+
```
326+
327+
_Parameters_
328+
329+
- _callback_ `T`: The callback function to wrap.
330+
308331
### useFocusableIframe
309332

310333
Dispatches a bubbling focus event when the iframe receives focus. Use `onFocus` as usual on the iframe or a parent element.
@@ -500,23 +523,30 @@ _Returns_
500523

501524
### useResizeObserver
502525

503-
Hook which allows to listen to the resize event of any target element when it changes size. \_Note: `useResizeObserver` will report `null` sizes until after first render.
526+
Sets up a [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Resize_Observer_API) for an HTML or SVG element.
527+
528+
Pass the returned setter as a callback ref to the React element you want to observe, or use it in layout effects for advanced use cases.
504529

505530
_Usage_
506531

507-
```js
508-
const App = () => {
509-
const [ resizeListener, sizes ] = useResizeObserver();
532+
```tsx
533+
const setElement = useResizeObserver(
534+
( resizeObserverEntries ) => console.log( resizeObserverEntries ),
535+
{ box: 'border-box' }
536+
);
537+
<div ref={ setElement } />;
510538

511-
return (
512-
<div>
513-
{ resizeListener }
514-
Your content here
515-
</div>
516-
);
517-
};
539+
// The setter can be used in other ways, for example:
540+
useLayoutEffect( () => {
541+
setElement( document.querySelector( `data-element-id="${ elementId }"` ) );
542+
}, [ elementId ] );
518543
```
519544

545+
_Parameters_
546+
547+
- _callback_ `ResizeObserverCallback`: The `ResizeObserver` callback - [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver#callback).
548+
- _options_ `ResizeObserverOptions`: Options passed to `ResizeObserver.observe` when called - [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/observe#options). Changes will be ignored.
549+
520550
### useStateWithHistory
521551

522552
useState with undo/redo history.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* WordPress dependencies
3+
*/
4+
import { useRef, useInsertionEffect, useCallback } from '@wordpress/element';
5+
6+
/**
7+
* Any function.
8+
*/
9+
export type AnyFunction = ( ...args: any ) => any;
10+
11+
/**
12+
* Creates a stable callback function that has access to the latest state and
13+
* can be used within event handlers and effect callbacks. Throws when used in
14+
* the render phase.
15+
*
16+
* @param callback The callback function to wrap.
17+
*
18+
* @example
19+
*
20+
* ```tsx
21+
* function Component( props ) {
22+
* const onClick = useEvent( props.onClick );
23+
* useEffect( () => {
24+
* onClick();
25+
* // Won't trigger the effect again when props.onClick is updated.
26+
* }, [ onClick ] );
27+
* // Won't re-render Button when props.onClick is updated (if `Button` is
28+
* // wrapped in `React.memo`).
29+
* return <Button onClick={ onClick } />;
30+
* }
31+
* ```
32+
*/
33+
export default function useEvent< T extends AnyFunction >(
34+
/**
35+
* The callback function to wrap.
36+
*/
37+
callback?: T
38+
) {
39+
const ref = useRef< AnyFunction | undefined >( () => {
40+
throw new Error(
41+
'Callbacks created with `useEvent` cannot be called during rendering.'
42+
);
43+
} );
44+
useInsertionEffect( () => {
45+
ref.current = callback;
46+
} );
47+
return useCallback< AnyFunction >(
48+
( ...args ) => ref.current?.( ...args ),
49+
[]
50+
) as T;
51+
}

packages/compose/src/hooks/use-resize-observer/index.native.js renamed to packages/compose/src/hooks/use-resize-observer/_legacy/index.native.js

File renamed without changes.

packages/compose/src/hooks/use-resize-observer/index.tsx renamed to packages/compose/src/hooks/use-resize-observer/_legacy/index.tsx

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@ import type { ReactElement } from 'react';
66
/**
77
* WordPress dependencies
88
*/
9-
import {
10-
useCallback,
11-
useLayoutEffect,
12-
useRef,
13-
useState,
14-
} from '@wordpress/element';
9+
import { useCallback, useRef, useState } from '@wordpress/element';
10+
/**
11+
* Internal dependencies
12+
*/
13+
import useResizeObserver from '../index';
1514

16-
type ObservedSize = {
15+
export type ObservedSize = {
1716
width: number | null;
1817
height: number | null;
1918
};
@@ -84,28 +83,10 @@ type ResizeElementProps = {
8483
};
8584

8685
function ResizeElement( { onResize }: ResizeElementProps ) {
87-
const resizeElementRef = useRef< HTMLDivElement >( null );
88-
const resizeCallbackRef = useRef( onResize );
89-
90-
useLayoutEffect( () => {
91-
resizeCallbackRef.current = onResize;
92-
}, [ onResize ] );
93-
94-
useLayoutEffect( () => {
95-
const resizeElement = resizeElementRef.current as HTMLDivElement;
96-
const resizeObserver = new ResizeObserver( ( entries ) => {
97-
for ( const entry of entries ) {
98-
const newSize = extractSize( entry );
99-
resizeCallbackRef.current( newSize );
100-
}
101-
} );
102-
103-
resizeObserver.observe( resizeElement );
104-
105-
return () => {
106-
resizeObserver.unobserve( resizeElement );
107-
};
108-
}, [] );
86+
const resizeElementRef = useResizeObserver( ( entries ) => {
87+
const newSize = extractSize( entries.at( -1 )! ); // Entries are never empty.
88+
onResize( newSize );
89+
} );
10990

11091
return (
11192
<div
@@ -141,7 +122,10 @@ const NULL_SIZE: ObservedSize = { width: null, height: null };
141122
* };
142123
* ```
143124
*/
144-
export default function useResizeObserver(): [ ReactElement, ObservedSize ] {
125+
export default function useLegacyResizeObserver(): [
126+
ReactElement,
127+
ObservedSize,
128+
] {
145129
const [ size, setSize ] = useState( NULL_SIZE );
146130

147131
// Using a ref to track the previous width / height to avoid unnecessary renders.

packages/compose/src/hooks/use-resize-observer/test/index.native.js renamed to packages/compose/src/hooks/use-resize-observer/_legacy/test/index.native.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { View } from 'react-native';
77
/**
88
* Internal dependencies
99
*/
10-
import useResizeObserver from '../';
10+
import useResizeObserver from '..';
1111

1212
const TestComponent = ( { onLayout } ) => {
1313
const [ resizeObserver, sizes ] = useResizeObserver();
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* WordPress dependencies
3+
*/
4+
import { useRef } from '@wordpress/element';
5+
/**
6+
* Internal dependencies
7+
*/
8+
import useEvent from '../use-event';
9+
import type { ObservedSize } from './_legacy';
10+
import _useLegacyResizeObserver from './_legacy';
11+
/**
12+
* External dependencies
13+
*/
14+
import type { ReactElement } from 'react';
15+
16+
// This is the current implementation of `useResizeObserver`.
17+
//
18+
// The legacy implementation is still supported for backwards compatibility.
19+
// This is achieved by overloading the exported function with both signatures,
20+
// and detecting which API is being used at runtime.
21+
function _useResizeObserver< T extends HTMLElement >(
22+
callback: ResizeObserverCallback,
23+
resizeObserverOptions: ResizeObserverOptions = {}
24+
): ( element?: T | null ) => void {
25+
const callbackEvent = useEvent( callback );
26+
27+
const observedElementRef = useRef< T | null >();
28+
const resizeObserverRef = useRef< ResizeObserver >();
29+
return useEvent( ( element?: T | null ) => {
30+
if ( element === observedElementRef.current ) {
31+
return;
32+
}
33+
observedElementRef.current = element;
34+
35+
// Set up `ResizeObserver`.
36+
resizeObserverRef.current ??= new ResizeObserver( callbackEvent );
37+
const { current: resizeObserver } = resizeObserverRef;
38+
39+
// Unobserve previous element.
40+
if ( observedElementRef.current ) {
41+
resizeObserver.unobserve( observedElementRef.current );
42+
}
43+
44+
// Observe new element.
45+
if ( element ) {
46+
resizeObserver.observe( element, resizeObserverOptions );
47+
}
48+
} );
49+
}
50+
51+
/**
52+
* Sets up a [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Resize_Observer_API)
53+
* for an HTML or SVG element.
54+
*
55+
* Pass the returned setter as a callback ref to the React element you want
56+
* to observe, or use it in layout effects for advanced use cases.
57+
*
58+
* @example
59+
*
60+
* ```tsx
61+
* const setElement = useResizeObserver(
62+
* ( resizeObserverEntries ) => console.log( resizeObserverEntries ),
63+
* { box: 'border-box' }
64+
* );
65+
* <div ref={ setElement } />;
66+
*
67+
* // The setter can be used in other ways, for example:
68+
* useLayoutEffect( () => {
69+
* setElement( document.querySelector( `data-element-id="${ elementId }"` ) );
70+
* }, [ elementId ] );
71+
* ```
72+
*
73+
* @param callback The `ResizeObserver` callback - [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver#callback).
74+
* @param options Options passed to `ResizeObserver.observe` when called - [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/observe#options). Changes will be ignored.
75+
*/
76+
export default function useResizeObserver< T extends Element >(
77+
/**
78+
* The `ResizeObserver` callback - [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver#callback).
79+
*/
80+
callback: ResizeObserverCallback,
81+
/**
82+
* Options passed to `ResizeObserver.observe` when called - [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/observe#options). Changes will be ignored.
83+
*/
84+
options?: ResizeObserverOptions
85+
): ( element?: T | null ) => void;
86+
87+
/**
88+
* **This is a legacy API and should not be used.**
89+
*
90+
* @deprecated Use the other `useResizeObserver` API instead: `const ref = useResizeObserver( ( entries ) => { ... } )`.
91+
*
92+
* Hook which allows to listen to the resize event of any target element when it changes size.
93+
* _Note: `useResizeObserver` will report `null` sizes until after first render.
94+
*
95+
* @example
96+
*
97+
* ```js
98+
* const App = () => {
99+
* const [ resizeListener, sizes ] = useResizeObserver();
100+
*
101+
* return (
102+
* <div>
103+
* { resizeListener }
104+
* Your content here
105+
* </div>
106+
* );
107+
* };
108+
* ```
109+
*/
110+
export default function useResizeObserver(): [ ReactElement, ObservedSize ];
111+
112+
export default function useResizeObserver< T extends HTMLElement >(
113+
callback?: ResizeObserverCallback,
114+
options: ResizeObserverOptions = {}
115+
): ( ( element?: T | null ) => void ) | [ ReactElement, ObservedSize ] {
116+
return callback
117+
? _useResizeObserver( callback, options )
118+
: _useLegacyResizeObserver();
119+
}

packages/compose/src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export { default as useCopyOnClick } from './hooks/use-copy-on-click';
2525
export { default as useCopyToClipboard } from './hooks/use-copy-to-clipboard';
2626
export { default as __experimentalUseDialog } from './hooks/use-dialog';
2727
export { default as useDisabled } from './hooks/use-disabled';
28+
export { default as useEvent } from './hooks/use-event';
2829
export { default as __experimentalUseDragging } from './hooks/use-dragging';
2930
export { default as useFocusOnMount } from './hooks/use-focus-on-mount';
3031
export { default as __experimentalUseFocusOutside } from './hooks/use-focus-outside';

packages/docgen/lib/get-type-annotation.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,10 @@ function getTypeAnnotation( typeAnnotation ) {
401401
* TODO: Remove the special-casing here once we're able to infer the types from TypeScript itself.
402402
*/
403403
function unwrapWrappedSelectors( token ) {
404+
if ( babelTypes.isTSDeclareFunction( token ) ) {
405+
return token;
406+
}
407+
404408
if ( babelTypes.isFunctionDeclaration( token ) ) {
405409
return token;
406410
}

0 commit comments

Comments
 (0)