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

Compose: useMergeRefs: add test for disabling refs + better docs #32044

Merged
merged 2 commits into from
May 21, 2021
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
43 changes: 36 additions & 7 deletions packages/compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,13 +305,42 @@ _Returns_

<a name="useMergeRefs" href="#useMergeRefs">#</a> **useMergeRefs**

Merges refs into one ref callback. Ensures the merged ref callbacks are only
called when it changes (as a result of a `useCallback` dependency update) or
when the ref value changes. If you don't wish a ref callback to be called on
every render, wrap it with `useCallback( ref, [] )`.
Dependencies can be added, but when a dependency changes, the old ref
callback will be called with `null` and the new ref callback will be called
with the same node.
Merges refs into one ref callback.

It also ensures that the merged ref callbacks are only called when they
change (as a result of a `useCallback` dependency update) OR when the ref
value changes, just as React does when passing a single ref callback to the
component.

As expected, if you pass a new function on every render, the ref callback
will be called after every render.

If you don't wish a ref callback to be called after every render, wrap it
with `useCallback( callback, dependencies )`. When a dependency changes, the
old ref callback will be called with `null` and the new ref callback will be
called with the same value.

To make ref callbacks easier to use, you can also pass the result of
`useRefEffect`, which makes cleanup easier by allowing you to return a
cleanup function instead of handling `null`.

It's also possible to _disable_ a ref (and its behaviour) by simply not
passing the ref.

```jsx
const ref = useRefEffect( ( node ) => {
node.addEventListener( ... );
return () => {
node.removeEventListener( ... );
};
}, [ ...dependencies ] );
const otherRef = useRef();
const mergedRefs useMergeRefs( [
enabled && ref,
otherRef,
] );
return <div ref={ mergedRefs } />;
```

_Parameters_

Expand Down
43 changes: 36 additions & 7 deletions packages/compose/src/hooks/use-merge-refs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,42 @@ function assignRef( ref, value ) {
}

/**
* Merges refs into one ref callback. Ensures the merged ref callbacks are only
* called when it changes (as a result of a `useCallback` dependency update) or
* when the ref value changes. If you don't wish a ref callback to be called on
* every render, wrap it with `useCallback( ref, [] )`.
* Dependencies can be added, but when a dependency changes, the old ref
* callback will be called with `null` and the new ref callback will be called
* with the same node.
* Merges refs into one ref callback.
*
* It also ensures that the merged ref callbacks are only called when they
* change (as a result of a `useCallback` dependency update) OR when the ref
* value changes, just as React does when passing a single ref callback to the
* component.
*
* As expected, if you pass a new function on every render, the ref callback
* will be called after every render.
*
* If you don't wish a ref callback to be called after every render, wrap it
* with `useCallback( callback, dependencies )`. When a dependency changes, the
* old ref callback will be called with `null` and the new ref callback will be
* called with the same value.
*
* To make ref callbacks easier to use, you can also pass the result of
* `useRefEffect`, which makes cleanup easier by allowing you to return a
* cleanup function instead of handling `null`.
*
* It's also possible to _disable_ a ref (and its behaviour) by simply not
* passing the ref.
*
* ```jsx
* const ref = useRefEffect( ( node ) => {
* node.addEventListener( ... );
* return () => {
* node.removeEventListener( ... );
* };
* }, [ ...dependencies ] );
* const otherRef = useRef();
* const mergedRefs useMergeRefs( [
* enabled && ref,
* otherRef,
* ] );
* return <div ref={ mergedRefs } />;
* ```
*
* @param {Array<RefObject|RefCallback>} refs The refs to be merged.
*
Expand Down
72 changes: 63 additions & 9 deletions packages/compose/src/hooks/use-merge-refs/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ describe( 'useMergeRefs', () => {

renderCallback.history = [];

function MergedRefs( { count, tagName: TagName = 'div' } ) {
function MergedRefs( {
count,
tagName: TagName = 'div',
disable1,
disable2,
} ) {
function refCallback1( value ) {
refCallback1.history.push( value );
}
Expand All @@ -51,14 +56,14 @@ describe( 'useMergeRefs', () => {

renderCallback( [ refCallback1.history, refCallback2.history ] );

return (
<TagName
ref={ useMergeRefs( [
useCallback( refCallback1, [] ),
useCallback( refCallback2, [ count ] ),
] ) }
/>
);
const ref1 = useCallback( refCallback1, [] );
const ref2 = useCallback( refCallback2, [ count ] );
const mergedRefs = useMergeRefs( [
! disable1 && ref1,
! disable2 && ref2,
] );

return <TagName ref={ mergedRefs } />;
}

beforeEach( () => {
Expand Down Expand Up @@ -273,4 +278,53 @@ describe( 'useMergeRefs', () => {
[ [], [ newElement, null ] ],
] );
} );

it( 'should allow disabling a ref', () => {
const rootElement = document.getElementById( 'root' );

ReactDOM.render( <MergedRefs disable1 />, rootElement );

const originalElement = rootElement.firstElementChild;

// Render 1: ref 1 should be disabled.
expect( renderCallback.history ).toEqual( [
[ [], [ originalElement ] ],
] );

ReactDOM.render( <MergedRefs disable2 />, rootElement );

// Render 2: ref 1 should be enabled and receive the ref. Note that the
// callback hasn't changed, so the original callback function will be
// called. Ref 2 should be disabled, so called with null.
expect( renderCallback.history ).toEqual( [
[ [ originalElement ], [ originalElement, null ] ],
[ [], [] ],
] );

ReactDOM.render( <MergedRefs disable1 count={ 1 } />, rootElement );

// Render 3: ref 1 should again be disabled. Ref 2 to should receive a
// ref with the new callback function because the count has been
// changed.
expect( renderCallback.history ).toEqual( [
[
[ originalElement, null ],
[ originalElement, null ],
],
[ [], [] ],
[ [], [ originalElement ] ],
] );

ReactDOM.render( null, rootElement );

// Unmount: current callback functions should receive null.
expect( renderCallback.history ).toEqual( [
[
[ originalElement, null ],
[ originalElement, null ],
],
[ [], [] ],
[ [], [ originalElement, null ] ],
] );
} );
} );