11/**
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.
37 *
4- * (c) 2024 Feedzai
8+ * (c) 2023 Feedzai, Rights Reserved.
59 */
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" ;
812
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 ;
1216
1317/**
1418 * Assigns values to each ref
@@ -26,7 +30,7 @@ export function assignRef<Generic = HTMLElement>(ref: SingleRef<Generic>, value:
2630 try {
2731 ref . current = value ;
2832 } catch ( error ) {
29- throwError ( "helpers " , "useMergeRefs" , "Cannot assign value to ref" ) ;
33+ throwError ( "@feedzai/js-utilities " , "useMergeRefs" , "Cannot assign value to ref" ) ;
3034 }
3135}
3236
@@ -44,37 +48,121 @@ export function mergeRefs<Generic = HTMLElement>(
4448}
4549
4650/**
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.
4853 *
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.
56123 *
57124 * @example
58125 * ```tsx
59- * import { useMergeRefs } from '@feedzai/js-utilities/hooks';
60- * ...
61- * // a div with multiple refs
126+ * // Legacy two-parameter API (backwards compatible)
62127 * function Example({ ref, ...props }) {
63128 * const internalRef = React.useRef();
64129 * const refs = useMergeRefs(internalRef, ref);
65130 *
66131 * return (
67132 * <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}>
68146 * A div with multiple refs.
69147 * </div>
70148 * );
71149 * }
72150 * ```
73151 */
74152export 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 ) ;
80168}
0 commit comments