1
1
/**
2
- * Please refer to the terms of the license agreement in the root of the project
2
+ * The copyright of this file belongs to Feedzai. The file cannot be
3
+ * reproduced in whole or in part, stored in a retrieval system, transmitted
4
+ * in any form, or by any means electronic, mechanical, or otherwise, without
5
+ * the prior permission of the owner. Please refer to the terms of the license
6
+ * agreement.
3
7
*
4
- * (c) 2024 Feedzai
8
+ * (c) 2023 Feedzai, Rights Reserved.
5
9
*/
6
- import { useMemo } from "react" ;
7
- import { isNil , isFunction , throwError } from ".. /functions" ;
10
+ import { useMemo , useRef , useCallback } from "react" ;
11
+ import { isFunction , isNil , throwError } from "src /functions" ;
8
12
9
- export type ReactRef < Generic > = React . RefCallback < Generic > | React . MutableRefObject < Generic > ;
10
- export type SingleRef < Generic > = ReactRef < Generic > | null | undefined ;
11
- export type MergedRefCallback < Generic > = ( node : Generic | null ) => void ;
13
+ type ReactRef < Generic > = React . RefCallback < Generic > | React . MutableRefObject < Generic > ;
14
+ type SingleRef < Generic > = ReactRef < Generic > | null | undefined ;
15
+ type MergedRefCallback < Generic > = ( node : Generic | null ) => void ;
12
16
13
17
/**
14
18
* Assigns values to each ref
@@ -26,7 +30,7 @@ export function assignRef<Generic = HTMLElement>(ref: SingleRef<Generic>, value:
26
30
try {
27
31
ref . current = value ;
28
32
} catch ( error ) {
29
- throwError ( "helpers " , "useMergeRefs" , "Cannot assign value to ref" ) ;
33
+ throwError ( "@feedzai/js-utilities " , "useMergeRefs" , "Cannot assign value to ref" ) ;
30
34
}
31
35
}
32
36
@@ -44,37 +48,121 @@ export function mergeRefs<Generic = HTMLElement>(
44
48
}
45
49
46
50
/**
47
- * The useMergeRefs hook is designed to combine multiple React refs into a single callback ref.
51
+ * Merges an array of refs into a single memoized callback ref or `null`.
52
+ * Supports both cleanup functions returned by ref callbacks and proper cleanup on unmount.
48
53
*
49
- * This is particularly useful when you need to apply multiple refs to a single element, such as when working with
50
- * both internal component logic and external ref forwarding. By merging refs, you can maintain the functionality of
51
- * each individual ref while avoiding conflicts or overrides that might occur when attempting to assign multiple
52
- * refs directly.
53
- *
54
- * This hook enhances component flexibility and reusability, allowing developers to easily integrate both
55
- * component-specific refs and externally provided refs in a clean, efficient manner.
54
+ * @param refs - Array of refs to merge
55
+ * @returns A merged ref callback or null if all refs are null/undefined
56
+ */
57
+ export function useMergeArrayOfRefs < Generic = HTMLElement > (
58
+ refs : Array < SingleRef < Generic > | undefined >
59
+ ) : null | MergedRefCallback < Generic > {
60
+ const cleanupRef = useRef < void | ( ( ) => void ) > ( undefined ) ;
61
+
62
+ const refEffect = useCallback ( ( instance : Generic | null ) => {
63
+ const cleanups = refs . map ( ( ref ) => {
64
+ if ( ref === null || ref === undefined ) {
65
+ return undefined ;
66
+ }
67
+
68
+ if ( typeof ref === "function" ) {
69
+ const refCallback = ref ;
70
+
71
+ const refCleanup : void | ( ( ) => void ) = refCallback ( instance ) ;
72
+
73
+ return typeof refCleanup === "function"
74
+ ? refCleanup
75
+ : ( ) => {
76
+ refCallback ( null ) ;
77
+ } ;
78
+ }
79
+
80
+ ( ref as React . MutableRefObject < Generic | null > ) . current = instance ;
81
+ return ( ) => {
82
+ ( ref as React . MutableRefObject < Generic | null > ) . current = null ;
83
+ } ;
84
+ } ) ;
85
+
86
+ return ( ) => {
87
+ cleanups . forEach ( ( refCleanup ) => refCleanup ?.( ) ) ;
88
+ } ;
89
+ // eslint-disable-next-line react-hooks/exhaustive-deps
90
+ } , refs ) ;
91
+
92
+ return useMemo ( ( ) => {
93
+ if ( refs . every ( ( ref ) => isNil ( ref ) ) ) {
94
+ return null ;
95
+ }
96
+
97
+ return ( value : Generic | null ) => {
98
+ if ( cleanupRef . current ) {
99
+ cleanupRef . current ( ) ;
100
+ cleanupRef . current = undefined ;
101
+ }
102
+
103
+ if ( value !== null ) {
104
+ cleanupRef . current = refEffect ( value ) ;
105
+ }
106
+ } ;
107
+ // eslint-disable-next-line react-hooks/exhaustive-deps
108
+ } , refs ) ;
109
+ }
110
+
111
+ // Overloaded function signatures for useMergeRefs
112
+ export function useMergeRefs < Generic = HTMLElement > (
113
+ refs : Array < SingleRef < Generic > | undefined >
114
+ ) : null | MergedRefCallback < Generic > ;
115
+ export function useMergeRefs < Generic = HTMLElement > (
116
+ firstRef : SingleRef < Generic > ,
117
+ secondRef : SingleRef < Generic >
118
+ ) : MergedRefCallback < Generic > ;
119
+
120
+ /**
121
+ * Returns a function that receives the element and assigns the value to the given React refs.
122
+ * Supports both the legacy two-parameter API and the new array API.
56
123
*
57
124
* @example
58
125
* ```tsx
59
- * import { useMergeRefs } from '@feedzai/js-utilities/hooks';
60
- * ...
61
- * // a div with multiple refs
126
+ * // Legacy two-parameter API (backwards compatible)
62
127
* function Example({ ref, ...props }) {
63
128
* const internalRef = React.useRef();
64
129
* const refs = useMergeRefs(internalRef, ref);
65
130
*
66
131
* return (
67
132
* <div {...props} ref={refs}>
133
+ * A div with two refs.
134
+ * </div>
135
+ * );
136
+ * }
137
+ *
138
+ * // New array API (supports any number of refs)
139
+ * function Example({ ref, ...props }) {
140
+ * const internalRef = React.useRef();
141
+ * const anotherRef = React.useRef();
142
+ * const refs = useMergeRefs([internalRef, ref, anotherRef]);
143
+ *
144
+ * return (
145
+ * <div {...props} ref={refs}>
68
146
* A div with multiple refs.
69
147
* </div>
70
148
* );
71
149
* }
72
150
* ```
73
151
*/
74
152
export function useMergeRefs < Generic = HTMLElement > (
75
- firstRef : SingleRef < Generic > ,
76
- secondRef : SingleRef < Generic >
77
- ) : MergedRefCallback < Generic > {
78
- // eslint-disable-next-line react-hooks/exhaustive-deps
79
- return useMemo ( ( ) => mergeRefs ( firstRef , secondRef ) , [ firstRef , secondRef ] ) ;
153
+ ...args : [ Array < SingleRef < Generic > | undefined > ] | [ SingleRef < Generic > , SingleRef < Generic > ]
154
+ ) : null | MergedRefCallback < Generic > {
155
+ // Normalize arguments to always use array format to avoid conditional hooks
156
+ const refsArray = useMemo ( ( ) => {
157
+ if ( Array . isArray ( args [ 0 ] ) ) {
158
+ return args [ 0 ] ;
159
+ }
160
+ // Legacy two-parameter API
161
+ const [ firstRef , secondRef ] = args as [ SingleRef < Generic > , SingleRef < Generic > ] ;
162
+
163
+ return [ firstRef , secondRef ] ;
164
+ } , [ args ] ) ;
165
+
166
+ // Always use the array implementation
167
+ return useMergeArrayOfRefs ( refsArray ) ;
80
168
}
0 commit comments