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

Add useEvent and revamped useResizeObserver to @wordpress/compose #64943

Merged
merged 32 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
95881cc
Simplify useResizeObserver
jsnajdr Aug 27, 2024
1e305a9
Loop through all resize entries
jsnajdr Aug 30, 2024
55172e3
Add `useEvent` util.
DaniGuardiola Aug 30, 2024
0b91189
Add `useObserveElementSize` util.
DaniGuardiola Aug 30, 2024
c4ee26c
Simplify `useResizeObserver` by using `useEvent` and `useObserveEleme…
DaniGuardiola Aug 30, 2024
575423c
Merge branch 'trunk' of https://github.com/WordPress/gutenberg into f…
DaniGuardiola Aug 30, 2024
93a36e1
Switch to layout effect and accept refs too.
DaniGuardiola Sep 2, 2024
fc33849
Prevent initial re-render in ResizeElement.
DaniGuardiola Sep 2, 2024
b422ccc
Better error message.
DaniGuardiola Sep 2, 2024
cff6661
Improved example of useEvent.
DaniGuardiola Sep 2, 2024
9531c10
Update packages/compose/src/hooks/use-event/index.ts
DaniGuardiola Sep 2, 2024
c58e10b
Sync docs.
DaniGuardiola Sep 2, 2024
0da2846
Avoid redundant resize listener calls.
DaniGuardiola Sep 2, 2024
bc77100
Switch to structural check.
DaniGuardiola Sep 2, 2024
638f551
Improve example.
DaniGuardiola Sep 2, 2024
654bac5
Fix docs.
DaniGuardiola Sep 2, 2024
00f53f7
Make `useObserveElementSize` generic.
DaniGuardiola Sep 3, 2024
b6dc4f4
New API that returns a ref.
DaniGuardiola Sep 5, 2024
74c4a00
Make utility private for now.
DaniGuardiola Sep 5, 2024
4fb82b9
Mark legacy `useResizeObserver` as such.
DaniGuardiola Sep 5, 2024
a381140
Rename `useObserveElementSize` to `useResizeObserver`.
DaniGuardiola Sep 5, 2024
7b4ecce
Add return type.
DaniGuardiola Sep 5, 2024
f42603c
Add signature as overload.
DaniGuardiola Sep 5, 2024
63deb16
Add support for legacy API.
DaniGuardiola Sep 5, 2024
30a07f0
Move into subdirectory.
DaniGuardiola Sep 5, 2024
e57cc0e
Minor import fix.
DaniGuardiola Sep 5, 2024
15eaebc
Fix docgen to support overloads (will pick up the first function sign…
DaniGuardiola Sep 5, 2024
3387a0a
Replace legacy utility with the new one.
DaniGuardiola Sep 6, 2024
8c2094f
Apply feedback.
DaniGuardiola Sep 8, 2024
6f3abb7
Clean up and document.
DaniGuardiola Sep 8, 2024
4aa01b3
Added changelog entries.
DaniGuardiola Sep 9, 2024
55946ef
Merge branch 'trunk' of https://github.com/WordPress/gutenberg into f…
DaniGuardiola Sep 9, 2024
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
66 changes: 56 additions & 10 deletions packages/compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,29 @@ _Returns_

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

### useEvent

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.

_Usage_

```tsx
function Component( props ) {
const onClick = useEvent( props.onClick );
useEffect( () => {
onClick();
// Won't trigger the effect again when props.onClick is updated.
}, [ onClick ] );
// Won't re-render Button when props.onClick is updated (if `Button` is
// wrapped in `React.memo`).
return <Button onClick={ onClick } />;
}
```

_Parameters_

- _callback_ `T`: The callback function to wrap.

### useFocusableIframe

Dispatches a bubbling focus event when the iframe receives focus. Use `onFocus` as usual on the iframe or a parent element.
Expand Down Expand Up @@ -500,23 +523,46 @@ _Returns_

### useResizeObserver

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.
Tracks a given element's size and calls `onUpdate` for all of its discrete values using a `ResizeObserver`. Pass the returned ref to the element or pass the element to the `targetElement` option directly.

_Usage_

```js
const App = () => {
const [ resizeListener, sizes ] = useResizeObserver();
```tsx
const targetElementRef = useResizeObserver(
( resizeObserverEntries, element ) => {
console.log( 'Resize observer entries:', resizeObserverEntries );
console.log( 'Element that was measured:', element );
},
{ box: 'border-box' }
);
<div ref={ targetElementRef } />;

// Alternatively, pass the element directly as an argument:
const [ targetElement, setTargetElement ] = useState< HTMLElement | null >();
useResizeObserver(
// ...
{
targetElement,
// ...
}
);
<div ref={ setTargetElement } />;
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved

return (
<div>
{ resizeListener }
Your content here
</div>
// The element could be obtained through other means, for example:
useEffect( () => {
const element = document.querySelector(
`[data-element-id="${ elementId }"]`
);
};
setTargetElement( element );
}, [ elementId ] );
```

_Parameters_

- _onUpdate_ `( resizeObserverEntries: ResizeObserverEntry[], element: T ) => void`: Callback that will be called when the element is measured (initially and after resizes).
- _options_ `ObserveElementSizeOptions< T >`: Options that, with the exception of `targetElement`, will be passed to `ResizeObserver.observe` when called internally. Updating them will not cause the observer to be re-created, and they will only take effect if a new element is observed.
- _options.targetElement_ `ObserveElementSizeOptions< T >`: The target element to observe. This parameter is an alternative to the returned ref. The element can be changed dynamically.

### useStateWithHistory

useState with undo/redo history.
Expand Down
51 changes: 51 additions & 0 deletions packages/compose/src/hooks/use-event/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* WordPress dependencies
*/
import { useRef, useInsertionEffect, useCallback } from '@wordpress/element';

/**
* Any function.
*/
export type AnyFunction = ( ...args: any ) => any;

/**
* 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.
*
* @param callback The callback function to wrap.
*
* @example
*
* ```tsx
* function Component( props ) {
* const onClick = useEvent( props.onClick );
* useEffect( () => {
* onClick();
* // Won't trigger the effect again when props.onClick is updated.
* }, [ onClick ] );
* // Won't re-render Button when props.onClick is updated (if `Button` is
* // wrapped in `React.memo`).
* return <Button onClick={ onClick } />;
* }
* ```
*/
export default function useEvent< T extends AnyFunction >(
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
/**
* The callback function to wrap.
*/
callback?: T
) {
const ref = useRef< AnyFunction | undefined >( () => {
throw new Error(
'Callbacks created with `useEvent` cannot be called during rendering.'
);
} );
useInsertionEffect( () => {
ref.current = callback;
} );
return useCallback< AnyFunction >(
( ...args ) => ref.current?.( ...args ),
[]
) as T;
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ import type { ReactElement } from 'react';
/**
* WordPress dependencies
*/
import {
useCallback,
useLayoutEffect,
useRef,
useState,
} from '@wordpress/element';
import { useCallback, useRef, useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import useResizeObserver from '../index';
DaniGuardiola marked this conversation as resolved.
Show resolved Hide resolved

type ObservedSize = {
export type ObservedSize = {
width: number | null;
height: number | null;
};
Expand Down Expand Up @@ -84,28 +83,10 @@ type ResizeElementProps = {
};

function ResizeElement( { onResize }: ResizeElementProps ) {
const resizeElementRef = useRef< HTMLDivElement >( null );
const resizeCallbackRef = useRef( onResize );

useLayoutEffect( () => {
resizeCallbackRef.current = onResize;
}, [ onResize ] );

useLayoutEffect( () => {
const resizeElement = resizeElementRef.current as HTMLDivElement;
const resizeObserver = new ResizeObserver( ( entries ) => {
for ( const entry of entries ) {
const newSize = extractSize( entry );
resizeCallbackRef.current( newSize );
}
} );

resizeObserver.observe( resizeElement );

return () => {
resizeObserver.unobserve( resizeElement );
};
}, [] );
const resizeElementRef = useResizeObserver( ( entries ) => {
const newSize = extractSize( entries.at( -1 )! ); // Entries are never empty.
onResize( newSize );
} );

return (
<div
Expand Down Expand Up @@ -141,7 +122,10 @@ const NULL_SIZE: ObservedSize = { width: null, height: null };
* };
* ```
*/
export default function useResizeObserver(): [ ReactElement, ObservedSize ] {
export default function useLegacyResizeObserver(): [
ReactElement,
ObservedSize,
] {
const [ size, setSize ] = useState( NULL_SIZE );

// Using a ref to track the previous width / height to avoid unnecessary renders.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { View } from 'react-native';
/**
* Internal dependencies
*/
import useResizeObserver from '../';
import useResizeObserver from '..';

const TestComponent = ( { onLayout } ) => {
const [ resizeObserver, sizes ] = useResizeObserver();
Expand Down
Loading
Loading