Skip to content

Commit c809e39

Browse files
github-actions[bot]DiegoAndaiaarongarciah
authored
[utils] Support cleanup callbacks in useForkRef (@DiegoAndai) (#45733)
Co-authored-by: Diego Andai <[email protected]> Co-authored-by: Aarón García Hervás <[email protected]>
1 parent e6c1d79 commit c809e39

File tree

2 files changed

+89
-12
lines changed

2 files changed

+89
-12
lines changed

Diff for: packages/mui-utils/src/useForkRef/useForkRef.test.js

+49
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from 'react';
22
import { expect } from 'chai';
3+
import { spy } from 'sinon';
34
import { createRenderer, screen } from '@mui/internal-test-utils';
45
import useForkRef from './useForkRef';
56
import getReactElementRef from '../getReactElementRef';
@@ -108,4 +109,52 @@ describe('useForkRef', () => {
108109
expect(secondRightRef.current.id).to.equal('test');
109110
});
110111
});
112+
113+
it('calls clean up function if it exists', () => {
114+
const cleanUp = spy();
115+
const setup = spy();
116+
const setup2 = spy();
117+
const nullHandler = spy();
118+
119+
function onRefChangeWithCleanup(ref) {
120+
if (ref) {
121+
setup(ref.id);
122+
} else {
123+
nullHandler();
124+
}
125+
return cleanUp;
126+
}
127+
128+
function onRefChangeWithoutCleanup(ref) {
129+
if (ref) {
130+
setup2(ref.id);
131+
} else {
132+
nullHandler();
133+
}
134+
}
135+
136+
function App() {
137+
const ref = useForkRef(onRefChangeWithCleanup, onRefChangeWithoutCleanup);
138+
return <div id="test" ref={ref} />;
139+
}
140+
141+
const { unmount } = render(<App />);
142+
143+
expect(setup.args[0][0]).to.equal('test');
144+
expect(setup.callCount).to.equal(1);
145+
expect(cleanUp.callCount).to.equal(0);
146+
147+
expect(setup2.args[0][0]).to.equal('test');
148+
expect(setup2.callCount).to.equal(1);
149+
150+
unmount();
151+
152+
expect(setup.callCount).to.equal(1);
153+
expect(cleanUp.callCount).to.equal(1);
154+
155+
// Setup was not called again
156+
expect(setup2.callCount).to.equal(1);
157+
// Null handler hit because no cleanup is returned
158+
expect(nullHandler.callCount).to.equal(1);
159+
});
111160
});

Diff for: packages/mui-utils/src/useForkRef/useForkRef.ts

+40-12
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
'use client';
22
import * as React from 'react';
3-
import setRef from '../setRef';
43

54
/**
6-
* Takes an array of refs and returns a new ref which will apply any modification to all of the refs.
7-
* This is useful when you want to have the ref used in multiple places.
5+
* Merges refs into a single memoized callback ref or `null`.
86
*
97
* ```tsx
108
* const rootRef = React.useRef<Instance>(null);
@@ -21,20 +19,50 @@ import setRef from '../setRef';
2119
export default function useForkRef<Instance>(
2220
...refs: Array<React.Ref<Instance> | undefined>
2321
): React.RefCallback<Instance> | null {
24-
/**
25-
* This will create a new function if the refs passed to this hook change and are all defined.
26-
* This means react will call the old forkRef with `null` and the new forkRef
27-
* with the ref. Cleanup naturally emerges from this behavior.
28-
*/
22+
const cleanupRef = React.useRef<void | (() => void)>(undefined);
23+
24+
const refEffect = React.useCallback((instance: Instance | null) => {
25+
const cleanups = refs.map((ref) => {
26+
if (ref == null) {
27+
return null;
28+
}
29+
30+
if (typeof ref === 'function') {
31+
const refCallback = ref;
32+
const refCleanup: void | (() => void) = refCallback(instance);
33+
return typeof refCleanup === 'function'
34+
? refCleanup
35+
: () => {
36+
refCallback(null);
37+
};
38+
}
39+
40+
(ref as React.RefObject<Instance | null>).current = instance;
41+
return () => {
42+
(ref as React.RefObject<Instance | null>).current = null;
43+
};
44+
});
45+
46+
return () => {
47+
cleanups.forEach((refCleanup) => refCleanup?.());
48+
};
49+
// eslint-disable-next-line react-hooks/exhaustive-deps
50+
}, refs);
51+
2952
return React.useMemo(() => {
3053
if (refs.every((ref) => ref == null)) {
3154
return null;
3255
}
3356

34-
return (instance) => {
35-
refs.forEach((ref) => {
36-
setRef(ref, instance);
37-
});
57+
return (value) => {
58+
if (cleanupRef.current) {
59+
cleanupRef.current();
60+
(cleanupRef as React.RefObject<void | (() => void)>).current = undefined;
61+
}
62+
63+
if (value != null) {
64+
(cleanupRef as React.RefObject<void | (() => void)>).current = refEffect(value);
65+
}
3866
};
3967
// TODO: uncomment once we enable eslint-plugin-react-compiler // eslint-disable-next-line react-compiler/react-compiler -- intentionally ignoring that the dependency array must be an array literal
4068
// eslint-disable-next-line react-hooks/exhaustive-deps

0 commit comments

Comments
 (0)