@@ -41,6 +41,7 @@ import {
4141 Fragment ,
4242} from './ReactWorkTags' ;
4343import isArray from 'shared/isArray' ;
44+ import assign from 'shared/assign' ;
4445import { checkPropStringCoercion } from 'shared/CheckStringCoercion' ;
4546import { enableRefAsProp } from 'shared/ReactFeatureFlags' ;
4647
@@ -149,128 +150,165 @@ function unwrapThenable<T>(thenable: Thenable<T>): T {
149150 return trackUsedThenable ( thenableState , thenable , index ) ;
150151}
151152
153+ type CoercedStringRef = ( ( handle : mixed ) => void ) & { _stringRef : ?string , ...} ;
154+
155+ function convertStringRefToCallbackRef (
156+ returnFiber : Fiber ,
157+ current : Fiber | null ,
158+ element : ReactElement ,
159+ mixedRef : any ,
160+ ) : CoercedStringRef {
161+ const owner : ?Fiber = ( element . _owner : any ) ;
162+ if ( ! owner ) {
163+ if ( typeof mixedRef !== 'string' ) {
164+ throw new Error (
165+ 'Expected ref to be a function, a string, an object returned by React.createRef(), or null.' ,
166+ ) ;
167+ }
168+ throw new Error (
169+ `Element ref was specified as a string (${ mixedRef } ) but no owner was set. This could happen for one of` +
170+ ' the following reasons:\n' +
171+ '1. You may be adding a ref to a function component\n' +
172+ "2. You may be adding a ref to a component that was not created inside a component's render method\n" +
173+ '3. You have multiple copies of React loaded\n' +
174+ 'See https://reactjs.org/link/refs-must-have-owner for more information.' ,
175+ ) ;
176+ }
177+ if ( owner . tag !== ClassComponent ) {
178+ throw new Error (
179+ 'Function components cannot have string refs. ' +
180+ 'We recommend using useRef() instead. ' +
181+ 'Learn more about using refs safely here: ' +
182+ 'https://reactjs.org/link/strict-mode-string-ref' ,
183+ ) ;
184+ }
185+
186+ // At this point, we know the ref isn't an object or function but it could
187+ // be a number. Coerce it to a string.
188+ if ( __DEV__ ) {
189+ checkPropStringCoercion ( mixedRef , 'ref' ) ;
190+ }
191+ const stringRef = '' + mixedRef ;
192+
193+ if ( __DEV__ ) {
194+ if (
195+ // Will already warn with "Function components cannot be given refs"
196+ ! ( typeof element . type === 'function' && ! isReactClass ( element . type ) )
197+ ) {
198+ const componentName =
199+ getComponentNameFromFiber ( returnFiber ) || 'Component' ;
200+ if ( ! didWarnAboutStringRefs [ componentName ] ) {
201+ console . error (
202+ 'Component "%s" contains the string ref "%s". Support for string refs ' +
203+ 'will be removed in a future major release. We recommend using ' +
204+ 'useRef() or createRef() instead. ' +
205+ 'Learn more about using refs safely here: ' +
206+ 'https://reactjs.org/link/strict-mode-string-ref' ,
207+ componentName ,
208+ stringRef ,
209+ ) ;
210+ didWarnAboutStringRefs [ componentName ] = true ;
211+ }
212+ }
213+ }
214+
215+ const inst = owner . stateNode ;
216+ if ( ! inst ) {
217+ throw new Error (
218+ `Missing owner for string ref ${ stringRef } . This error is likely caused by a ` +
219+ 'bug in React. Please file an issue.' ,
220+ ) ;
221+ }
222+
223+ // Check if previous string ref matches new string ref
224+ if (
225+ current !== null &&
226+ current . ref !== null &&
227+ typeof current . ref === 'function' &&
228+ current . ref . _stringRef === stringRef
229+ ) {
230+ // Reuse the existing string ref
231+ const currentRef : CoercedStringRef = ( ( current . ref : any ) : CoercedStringRef ) ;
232+ return currentRef ;
233+ }
234+
235+ // Create a new string ref
236+ const ref = function ( value : mixed ) {
237+ const refs = inst . refs ;
238+ if ( value === null ) {
239+ delete refs [ stringRef ] ;
240+ } else {
241+ refs [ stringRef ] = value ;
242+ }
243+ } ;
244+ ref . _stringRef = stringRef ;
245+ return ref ;
246+ }
247+
152248function coerceRef (
153249 returnFiber : Fiber ,
154250 current : Fiber | null ,
251+ workInProgress : Fiber ,
155252 element : ReactElement ,
156- ) {
253+ ) : void {
157254 let mixedRef ;
158255 if ( enableRefAsProp ) {
159256 // TODO: This is a temporary, intermediate step. When enableRefAsProp is on,
160257 // we should resolve the `ref` prop during the begin phase of the component
161258 // it's attached to (HostComponent, ClassComponent, etc).
162-
163259 const refProp = element . props . ref ;
164260 mixedRef = refProp !== undefined ? refProp : null ;
165261 } else {
166262 // Old behavior.
167263 mixedRef = element . ref ;
168264 }
169265
266+ let coercedRef ;
170267 if (
171268 mixedRef !== null &&
172269 typeof mixedRef !== 'function' &&
173270 typeof mixedRef !== 'object'
174271 ) {
175- if ( __DEV__ ) {
176- if (
177- // Will already throw with "Function components cannot have string refs"
178- ! (
179- element . _owner &&
180- ( ( element . _owner : any ) : Fiber ) . tag !== ClassComponent
181- ) &&
182- // Will already warn with "Function components cannot be given refs"
183- ! ( typeof element . type === 'function' && ! isReactClass ( element . type ) ) &&
184- // Will already throw with "Element ref was specified as a string (someStringRef) but no owner was set"
185- element . _owner
186- ) {
187- const componentName =
188- getComponentNameFromFiber ( returnFiber ) || 'Component' ;
189- if ( ! didWarnAboutStringRefs [ componentName ] ) {
190- console . error (
191- 'Component "%s" contains the string ref "%s". Support for string refs ' +
192- 'will be removed in a future major release. We recommend using ' +
193- 'useRef() or createRef() instead. ' +
194- 'Learn more about using refs safely here: ' +
195- 'https://reactjs.org/link/strict-mode-string-ref' ,
196- componentName ,
197- mixedRef ,
198- ) ;
199- didWarnAboutStringRefs [ componentName ] = true ;
200- }
201- }
202- }
203-
204- if ( element . _owner ) {
205- const owner : ?Fiber = ( element . _owner : any ) ;
206- let inst ;
207- if ( owner ) {
208- const ownerFiber = ( ( owner : any ) : Fiber ) ;
209-
210- if ( ownerFiber . tag !== ClassComponent ) {
211- throw new Error (
212- 'Function components cannot have string refs. ' +
213- 'We recommend using useRef() instead. ' +
214- 'Learn more about using refs safely here: ' +
215- 'https://reactjs.org/link/strict-mode-string-ref' ,
216- ) ;
217- }
218-
219- inst = ownerFiber . stateNode ;
220- }
221-
222- if ( ! inst ) {
223- throw new Error (
224- `Missing owner for string ref ${ mixedRef } . This error is likely caused by a ` +
225- 'bug in React. Please file an issue.' ,
226- ) ;
227- }
228- // Assigning this to a const so Flow knows it won't change in the closure
229- const resolvedInst = inst ;
230-
231- if ( __DEV__ ) {
232- checkPropStringCoercion ( mixedRef , 'ref' ) ;
233- }
234- const stringRef = '' + mixedRef ;
235- // Check if previous string ref matches new string ref
236- if (
237- current !== null &&
238- current . ref !== null &&
239- typeof current . ref === 'function' &&
240- current . ref . _stringRef === stringRef
241- ) {
242- return current . ref ;
243- }
244- const ref = function ( value : mixed ) {
245- const refs = resolvedInst . refs ;
246- if ( value === null ) {
247- delete refs [ stringRef ] ;
248- } else {
249- refs [ stringRef ] = value ;
250- }
251- } ;
252- ref . _stringRef = stringRef ;
253- return ref ;
254- } else {
255- if ( typeof mixedRef !== 'string' ) {
256- throw new Error (
257- 'Expected ref to be a function, a string, an object returned by React.createRef(), or null.' ,
258- ) ;
259- }
272+ // Assume this is a string ref. If it's not, then this will throw an error
273+ // to the user.
274+ coercedRef = convertStringRefToCallbackRef (
275+ returnFiber ,
276+ current ,
277+ element ,
278+ mixedRef ,
279+ ) ;
260280
261- if ( ! element . _owner ) {
262- throw new Error (
263- `Element ref was specified as a string (${ mixedRef } ) but no owner was set. This could happen for one of` +
264- ' the following reasons:\n' +
265- '1. You may be adding a ref to a function component\n' +
266- "2. You may be adding a ref to a component that was not created inside a component's render method\n" +
267- '3. You have multiple copies of React loaded\n' +
268- 'See https://reactjs.org/link/refs-must-have-owner for more information.' ,
269- ) ;
270- }
281+ if ( enableRefAsProp ) {
282+ // When enableRefAsProp is on, we should always use the props as the
283+ // source of truth for refs. Not a field on the fiber.
284+ //
285+ // In the case of string refs, this presents a problem, because string
286+ // refs are not passed around internally as strings; they are converted to
287+ // callback refs. The ref used by the reconciler is not the same as the
288+ // one the user provided.
289+ //
290+ // But since this is a deprecated feature anyway, what we can do is clone
291+ // the props object and replace it with the internal callback ref. Then we
292+ // can continue to use the props object as the source of truth.
293+ //
294+ // This means the internal callback ref will leak into userspace. The
295+ // receiving component will receive a callback ref even though the parent
296+ // passed a string. Which is weird, but again, this is a deprecated
297+ // feature, and we're only leaving it around behind a flag so that Meta
298+ // can keep using string refs temporarily while they finish migrating
299+ // their codebase.
300+ const userProvidedProps = workInProgress . pendingProps ;
301+ const propsWithInternalCallbackRef = assign ( { } , userProvidedProps ) ;
302+ propsWithInternalCallbackRef . ref = coercedRef ;
303+ workInProgress . pendingProps = propsWithInternalCallbackRef ;
271304 }
305+ } else {
306+ coercedRef = mixedRef ;
272307 }
273- return mixedRef ;
308+
309+ // TODO: If enableRefAsProp is on, we shouldn't use the `ref` field. We
310+ // should always read the ref from the prop.
311+ workInProgress . ref = coercedRef ;
274312}
275313
276314function throwOnInvalidObjectType ( returnFiber : Fiber , newChild : Object ) {
@@ -537,7 +575,7 @@ function createChildReconciler(
537575 ) {
538576 // Move based on index
539577 const existing = useFiber ( current , element . props ) ;
540- existing . ref = coerceRef ( returnFiber , current , element ) ;
578+ coerceRef ( returnFiber , current , existing , element ) ;
541579 existing . return = returnFiber ;
542580 if ( __DEV__ ) {
543581 existing . _debugOwner = element . _owner ;
@@ -548,7 +586,7 @@ function createChildReconciler(
548586 }
549587 // Insert
550588 const created = createFiberFromElement ( element , returnFiber . mode , lanes ) ;
551- created . ref = coerceRef ( returnFiber , current , element ) ;
589+ coerceRef ( returnFiber , current , created , element ) ;
552590 created . return = returnFiber ;
553591 if ( __DEV__ ) {
554592 created . _debugInfo = debugInfo ;
@@ -652,7 +690,7 @@ function createChildReconciler(
652690 returnFiber . mode ,
653691 lanes ,
654692 ) ;
655- created . ref = coerceRef ( returnFiber , null , newChild ) ;
693+ coerceRef ( returnFiber , null , created , newChild ) ;
656694 created . return = returnFiber ;
657695 if ( __DEV__ ) {
658696 created . _debugInfo = mergeDebugInfo ( debugInfo , newChild . _debugInfo ) ;
@@ -1481,7 +1519,7 @@ function createChildReconciler(
14811519 ) {
14821520 deleteRemainingChildren ( returnFiber , child . sibling ) ;
14831521 const existing = useFiber ( child , element . props ) ;
1484- existing . ref = coerceRef ( returnFiber , child , element ) ;
1522+ coerceRef ( returnFiber , child , existing , element ) ;
14851523 existing . return = returnFiber ;
14861524 if ( __DEV__ ) {
14871525 existing . _debugOwner = element . _owner ;
@@ -1513,7 +1551,7 @@ function createChildReconciler(
15131551 return created ;
15141552 } else {
15151553 const created = createFiberFromElement ( element , returnFiber . mode , lanes ) ;
1516- created . ref = coerceRef ( returnFiber , currentFirstChild , element ) ;
1554+ coerceRef ( returnFiber , currentFirstChild , created , element ) ;
15171555 created . return = returnFiber ;
15181556 if ( __DEV__ ) {
15191557 created . _debugInfo = debugInfo ;
0 commit comments