4
4
forwardRef ,
5
5
Fragment ,
6
6
isValidElement ,
7
+ MutableRefObject ,
8
+ useCallback ,
9
+ useRef ,
7
10
type ElementType ,
8
11
type ReactElement ,
9
12
type Ref ,
@@ -54,6 +57,7 @@ export function render<TFeature extends Features, TTag extends ElementType, TSlo
54
57
features,
55
58
visible = true ,
56
59
name,
60
+ mergeRefs,
57
61
} : {
58
62
ourProps : Expand < Props < TTag , TSlot , any > & PropsForFeatures < TFeature > > & {
59
63
ref ?: Ref < HTMLElement | ElementType >
@@ -64,19 +68,22 @@ export function render<TFeature extends Features, TTag extends ElementType, TSlo
64
68
features ?: TFeature
65
69
visible ?: boolean
66
70
name : string
71
+ mergeRefs ?: ReturnType < typeof useMergeRefsFn >
67
72
} ) {
73
+ mergeRefs = mergeRefs ?? defaultMergeRefs
74
+
68
75
let props = mergeProps ( theirProps , ourProps )
69
76
70
77
// Visible always render
71
- if ( visible ) return _render ( props , slot , defaultTag , name )
78
+ if ( visible ) return _render ( props , slot , defaultTag , name , mergeRefs )
72
79
73
80
let featureFlags = features ?? Features . None
74
81
75
82
if ( featureFlags & Features . Static ) {
76
83
let { static : isStatic = false , ...rest } = props as PropsForFeatures < Features . Static >
77
84
78
85
// When the `static` prop is passed as `true`, then the user is in control, thus we don't care about anything else
79
- if ( isStatic ) return _render ( rest , slot , defaultTag , name )
86
+ if ( isStatic ) return _render ( rest , slot , defaultTag , name , mergeRefs )
80
87
}
81
88
82
89
if ( featureFlags & Features . RenderStrategy ) {
@@ -92,21 +99,23 @@ export function render<TFeature extends Features, TTag extends ElementType, TSlo
92
99
{ ...rest , ...{ hidden : true , style : { display : 'none' } } } ,
93
100
slot ,
94
101
defaultTag ,
95
- name
102
+ name ,
103
+ mergeRefs !
96
104
)
97
105
} ,
98
106
} )
99
107
}
100
108
101
109
// No features enabled, just render
102
- return _render ( props , slot , defaultTag , name )
110
+ return _render ( props , slot , defaultTag , name , mergeRefs )
103
111
}
104
112
105
113
function _render < TTag extends ElementType , TSlot > (
106
114
props : Props < TTag , TSlot > & { ref ?: unknown } ,
107
115
slot : TSlot = { } as TSlot ,
108
116
tag : ElementType ,
109
- name : string
117
+ name : string ,
118
+ mergeRefs : ReturnType < typeof useMergeRefsFn >
110
119
) {
111
120
let {
112
121
as : Component = tag ,
@@ -189,7 +198,7 @@ function _render<TTag extends ElementType, TSlot>(
189
198
mergeProps ( resolvedChildren . props as any , compact ( omit ( rest , [ 'ref' ] ) ) ) ,
190
199
dataAttributes ,
191
200
refRelatedProps ,
192
- mergeRefs ( ( resolvedChildren as any ) . ref , refRelatedProps . ref ) ,
201
+ { ref : mergeRefs ( ( resolvedChildren as any ) . ref , refRelatedProps . ref ) } ,
193
202
classNameProps
194
203
)
195
204
)
@@ -208,20 +217,57 @@ function _render<TTag extends ElementType, TSlot>(
208
217
)
209
218
}
210
219
211
- function mergeRefs ( ...refs : any [ ] ) {
212
- return {
213
- ref : refs . every ( ( ref ) => ref == null )
214
- ? undefined
215
- : ( value : any ) => {
216
- for ( let ref of refs ) {
217
- if ( ref == null ) continue
218
- if ( typeof ref === 'function' ) ref ( value )
219
- else ref . current = value
220
- }
221
- } ,
220
+ /**
221
+ * This is a singleton hook. **You can ONLY call the returned
222
+ * function *once* to produce expected results.** If you need
223
+ * to call `mergeRefs()` multiple times you need to create a
224
+ * separate function for each invocation. This happens as we
225
+ * store the list of `refs` to update and always return the
226
+ * same function that refers to that list of refs.
227
+ *
228
+ * You shouldn't normally read refs during render but this
229
+ * should actually be okay because React itself is calling
230
+ * the `function` that updates these refs and can only do
231
+ * so once the ref that contains the list is updated.
232
+ */
233
+ export function useMergeRefsFn ( ) {
234
+ type MaybeRef < T > = MutableRefObject < T > | ( ( value : T ) => void ) | null | undefined
235
+ let currentRefs = useRef < MaybeRef < any > [ ] > ( [ ] )
236
+ let mergedRef = useCallback ( ( value ) => {
237
+ for ( let ref of currentRefs . current ) {
238
+ if ( ref == null ) continue
239
+ if ( typeof ref === 'function' ) ref ( value )
240
+ else ref . current = value
241
+ }
242
+ } , [ ] )
243
+
244
+ return ( ...refs : any [ ] ) => {
245
+ if ( refs . every ( ( ref ) => ref == null ) ) {
246
+ return undefined
247
+ }
248
+
249
+ currentRefs . current = refs
250
+ return mergedRef
222
251
}
223
252
}
224
253
254
+ // This does not produce a stable function to use as a ref
255
+ // But we only use it in the case of as={Fragment}
256
+ // And it should really only re-render if setting the ref causes the parent to re-render unconditionally
257
+ // which then causes the child to re-render resulting in a render loop
258
+ // TODO: Add tests for this somehow
259
+ function defaultMergeRefs ( ...refs : any [ ] ) {
260
+ return refs . every ( ( ref ) => ref == null )
261
+ ? undefined
262
+ : ( value : any ) => {
263
+ for ( let ref of refs ) {
264
+ if ( ref == null ) continue
265
+ if ( typeof ref === 'function' ) ref ( value )
266
+ else ref . current = value
267
+ }
268
+ }
269
+ }
270
+
225
271
function mergeProps ( ...listOfProps : Props < any , any > [ ] ) {
226
272
if ( listOfProps . length === 0 ) return { }
227
273
if ( listOfProps . length === 1 ) return listOfProps [ 0 ]
0 commit comments