|
1 |
| -'use client'; |
2 | 1 | import * as React from 'react';
|
3 | 2 |
|
4 | 3 | /**
|
5 |
| - * Takes an array of refs and returns a new ref which will apply any modification to all of the refs. |
6 |
| - * This is useful when you want to have the ref used in multiple places. |
7 |
| - * |
8 |
| - * ```tsx |
9 |
| - * const rootRef = React.useRef<Instance>(null); |
10 |
| - * const refFork = useForkRef(rootRef, props.ref); |
11 |
| - * |
12 |
| - * return ( |
13 |
| - * <Root {...props} ref={refFork} /> |
14 |
| - * ); |
15 |
| - * ``` |
16 |
| - * |
17 |
| - * @param {Array<React.Ref<Instance> | undefined>} refs The ref array. |
18 |
| - * @returns {React.RefCallback<Instance> | null} The new ref callback. |
| 4 | + * Merges refs into a single memoized callback ref or `null`. |
19 | 5 | */
|
20 | 6 | export function useForkRef<Instance>(
|
21 | 7 | ...refs: Array<React.Ref<Instance> | undefined>
|
22 |
| -): React.RefCallback<Instance> | null { |
23 |
| - /** |
24 |
| - * This will create a new function if the refs passed to this hook change and are all defined. |
25 |
| - * This means react will call the old forkRef with `null` and the new forkRef |
26 |
| - * with the ref. Cleanup naturally emerges from this behavior. |
27 |
| - */ |
| 8 | +): null | React.RefCallback<Instance> { |
| 9 | + const cleanupRef = React.useRef<void | (() => void)>(undefined); |
| 10 | + |
| 11 | + const refEffect = React.useCallback((instance: Instance | null) => { |
| 12 | + const cleanups = refs.map((ref) => { |
| 13 | + if (ref == null) { |
| 14 | + return null; |
| 15 | + } |
| 16 | + |
| 17 | + if (typeof ref === 'function') { |
| 18 | + const refCallback = ref; |
| 19 | + const refCleanup: void | (() => void) = refCallback(instance); |
| 20 | + return typeof refCleanup === 'function' |
| 21 | + ? refCleanup |
| 22 | + : () => { |
| 23 | + refCallback(null); |
| 24 | + }; |
| 25 | + } |
| 26 | + |
| 27 | + (ref as React.RefObject<Instance | null>).current = instance; |
| 28 | + return () => { |
| 29 | + (ref as React.RefObject<Instance | null>).current = null; |
| 30 | + }; |
| 31 | + }); |
| 32 | + |
| 33 | + return () => { |
| 34 | + cleanups.forEach((refCleanup) => refCleanup?.()); |
| 35 | + }; |
| 36 | + // eslint-disable-next-line react-hooks/exhaustive-deps |
| 37 | + }, refs); |
| 38 | + |
28 | 39 | return React.useMemo(() => {
|
29 | 40 | if (refs.every((ref) => ref == null)) {
|
30 | 41 | return null;
|
31 | 42 | }
|
32 | 43 |
|
33 |
| - return (instance) => { |
34 |
| - refs.forEach((ref) => { |
35 |
| - setRef(ref, instance); |
36 |
| - }); |
| 44 | + return (value) => { |
| 45 | + if (cleanupRef.current) { |
| 46 | + cleanupRef.current(); |
| 47 | + (cleanupRef as React.RefObject<void | (() => void)>).current = undefined; |
| 48 | + } |
| 49 | + |
| 50 | + if (value != null) { |
| 51 | + (cleanupRef as React.RefObject<void | (() => void)>).current = refEffect(value); |
| 52 | + } |
37 | 53 | };
|
38 |
| - // 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 |
39 | 54 | // eslint-disable-next-line react-hooks/exhaustive-deps
|
40 | 55 | }, refs);
|
41 | 56 | }
|
42 |
| - |
43 |
| -function setRef<T>( |
44 |
| - ref: React.RefObject<T | null> | ((instance: T | null) => void) | null | undefined, |
45 |
| - value: T | null, |
46 |
| -): void { |
47 |
| - if (typeof ref === 'function') { |
48 |
| - ref(value); |
49 |
| - } else if (ref) { |
50 |
| - ref.current = value; |
51 |
| - } |
52 |
| -} |
|
0 commit comments