Move string ref coercion to JSX runtime (#28473)
Based on: - #28464 --- This moves the entire string ref implementation out Fiber and into the JSX runtime. The string is converted to a callback ref during element creation. This is a subtle change in behavior, because it will have already been converted to a callback ref if you access element.prop.ref or element.ref. But this is only for Meta, because string refs are disabled entirely in open source. And if it leads to an issue in practice, the solution is to switch to a different ref type, which Meta is going to do regardless.
Andrew Clark committed
Apr 5, 2024 at 10:53 UTC
e3ebcd54b98a4f8f5a9f7e63982fa75578b648ed
14 files changed
+199
-245
packages/react-dom/src/__tests__/ReactComponent-test.js
+5
-10
@@ -47,10 +47,9 @@ describe('ReactComponent', () => {
47
act(() => {
48
root.render(<div ref="badDiv" />);
49
}),
50
- ).rejects.toThrow(
51
- 'Element ref was specified as a string (badDiv) but no owner ' +
52
- 'was set',
53
- );
50
+ // TODO: This throws an AggregateError. Need to update test infra to
51
+ // support matching against AggregateError.
52
+ ).rejects.toThrow();
53
});
54
55
it('should throw (in dev) when children are mutated during render', async () => {
@@ -169,18 +168,14 @@ describe('ReactComponent', () => {
168
root.render(<Component />);
169
});
170
}).toErrorDev([
172
- 'Warning: Component "div" contains the string ref "inner". ' +
171
+ 'Warning: Component "Component" contains the string ref "inner". ' +
172
'Support for string refs will be removed in a future major release. ' +
173
'We recommend using useRef() or createRef() instead. ' +
174
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
175
+ ' in Wrapper (at **)\n' +
176
' in div (at **)\n' +
177
' in Wrapper (at **)\n' +
178
' in Component (at **)',
179
- 'Warning: Component "Component" contains the string ref "outer". ' +
180
- 'Support for string refs will be removed in a future major release. ' +
181
- 'We recommend using useRef() or createRef() instead. ' +
182
- 'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
183
- ' in Component (at **)',
179
]);
180
});
181
packages/react-dom/src/__tests__/ReactDOMServerIntegrationRefs-test.js
+1
@@ -100,6 +100,7 @@ describe('ReactDOMServerIntegration', () => {
100
'Support for string refs will be removed in a future major release. ' +
101
'We recommend using useRef() or createRef() instead. ' +
102
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
103
+ ' in div (at **)\n' +
104
' in RefsComponent (at **)',
105
]);
106
expect(component.refs.myDiv).toBe(root.firstChild);
packages/react-dom/src/__tests__/ReactDeprecationWarnings-test.js
+1
-8
@@ -86,6 +86,7 @@ describe('ReactDeprecationWarnings', () => {
86
'We recommend using useRef() or createRef() instead. ' +
87
'Learn more about using refs safely here: ' +
88
'https://react.dev/link/strict-mode-string-ref' +
89
+ '\n in RefComponent (at **)' +
90
'\n in Component (at **)',
91
);
92
});
@@ -137,10 +138,6 @@ describe('ReactDeprecationWarnings', () => {
138
'We ask you to manually fix this case by using useRef() or createRef() instead. ' +
139
'Learn more about using refs safely here: ' +
140
'https://react.dev/link/strict-mode-string-ref',
140
- 'Warning: Component "Component" contains the string ref "refComponent". ' +
141
- 'Support for string refs will be removed in a future major release. We recommend ' +
142
- 'using useRef() or createRef() instead. Learn more about using refs safely here: ' +
143
- 'https://react.dev/link/strict-mode-string-ref',
141
]);
142
});
143
@@ -173,10 +170,6 @@ describe('ReactDeprecationWarnings', () => {
170
'We ask you to manually fix this case by using useRef() or createRef() instead. ' +
171
'Learn more about using refs safely here: ' +
172
'https://react.dev/link/strict-mode-string-ref',
176
- 'Warning: Component "Component" contains the string ref "refComponent". ' +
177
- 'Support for string refs will be removed in a future major release. We recommend ' +
178
- 'using useRef() or createRef() instead. Learn more about using refs safely here: ' +
179
- 'https://react.dev/link/strict-mode-string-ref',
173
]);
174
});
175
});
packages/react-dom/src/__tests__/ReactFunctionComponent-test.js
+4
-12
@@ -185,18 +185,10 @@ describe('ReactFunctionComponent', () => {
185
act(() => {
186
root.render(<Child test="test" />);
187
}),
188
- ).rejects.toThrowError(
189
- __DEV__
190
- ? 'Function components cannot have string refs. We recommend using useRef() instead.'
191
- : // It happens because we don't save _owner in production for
192
- // function components.
193
- 'Element ref was specified as a string (me) but no owner was set. This could happen for one of' +
194
- ' the following reasons:\n' +
195
- '1. You may be adding a ref to a function component\n' +
196
- "2. You may be adding a ref to a component that was not created inside a component's render method\n" +
197
- '3. You have multiple copies of React loaded\n' +
198
- 'See https://react.dev/link/refs-must-have-owner for more information.',
199
- );
188
+ )
189
+ // TODO: This throws an AggregateError. Need to update test infra to
190
+ // support matching against AggregateError.
191
+ .rejects.toThrowError();
192
});
193
194
// @gate !enableRefAsProp || !__DEV__
packages/react-dom/src/__tests__/multiple-copies-of-react-test.js
+4
-8
@@ -30,13 +30,9 @@ describe('when different React version is used with string ref', () => {
30
act(() => {
31
root.render(<TextWithStringRef />);
32
}),
33
- ).rejects.toThrow(
34
- 'Element ref was specified as a string (foo) but no owner was set. This could happen for one of' +
35
- ' the following reasons:\n' +
36
- '1. You may be adding a ref to a function component\n' +
37
- "2. You may be adding a ref to a component that was not created inside a component's render method\n" +
38
- '3. You have multiple copies of React loaded\n' +
39
- 'See https://react.dev/link/refs-must-have-owner for more information.',
40
- );
33
+ )
34
+ // TODO: This throws an AggregateError. Need to update test infra to
35
+ // support matching against AggregateError.
36
+ .rejects.toThrow();
37
});
38
});
packages/react-dom/src/__tests__/refs-test.js
+30
-43
@@ -129,22 +129,22 @@ describe('reactiverefs', () => {
129
);
130
});
131
}).toErrorDev([
132
- 'Warning: Component "div" contains the string ref "resetDiv". ' +
133
- 'Support for string refs will be removed in a future major release. ' +
134
- 'We recommend using useRef() or createRef() instead. ' +
135
- 'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
136
- ' in div (at **)\n' +
137
- ' in TestRefsComponent (at **)',
138
- 'Warning: Component "span" contains the string ref "clickLog0". ' +
139
- 'Support for string refs will be removed in a future major release. ' +
140
- 'We recommend using useRef() or createRef() instead. ' +
141
- 'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
142
- ' in span (at **)\n' +
143
- ' in ClickCounter (at **)\n' +
132
+ 'Warning: Component "TestRefsComponent" contains the string ' +
133
+ 'ref "resetDiv". Support for string refs will be removed in a ' +
134
+ 'future major release. We recommend using useRef() or createRef() ' +
135
+ 'instead. Learn more about using refs safely ' +
136
+ 'here: https://react.dev/link/strict-mode-string-ref\n' +
137
' in div (at **)\n' +
145
- ' in GeneralContainerComponent (at **)\n' +
138
' in div (at **)\n' +
139
' in TestRefsComponent (at **)',
140
+ 'Warning: Component "ClickCounter" contains the string ' +
141
+ 'ref "clickLog0". Support for string refs will be removed in a ' +
142
+ 'future major release. We recommend using useRef() or createRef() ' +
143
+ 'instead. Learn more about using refs safely ' +
144
+ 'here: https://react.dev/link/strict-mode-string-ref\n' +
145
+ ' in div (at **)\n' +
146
+ ' in span (at **)\n' +
147
+ ' in ClickCounter (at **)',
148
]);
149
150
expect(testRefsComponent instanceof TestRefsComponent).toBe(true);
@@ -352,12 +352,12 @@ describe('ref swapping', () => {
352
'Support for string refs will be removed in a future major release. ' +
353
'We recommend using useRef() or createRef() instead. ' +
354
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
355
+ ' in div (at **)\n' +
356
' in A (at **)',
357
]);
358
expect(a.refs[1].nodeName).toBe('DIV');
359
});
360
360
- // @gate !disableStringRefs
361
it('provides an error for invalid refs', async () => {
362
const container = document.createElement('div');
363
const root = ReactDOMClient.createRoot(container);
@@ -365,16 +365,16 @@ describe('ref swapping', () => {
365
await act(() => {
366
root.render(<div ref={10} />);
367
});
368
- }).rejects.toThrow(
369
- 'Element ref was specified as a string (10) but no owner was set.',
370
- );
368
+ // TODO: This throws an AggregateError. Need to update test infra to
369
+ // support matching against AggregateError.
370
+ }).rejects.toThrow();
371
await expect(async () => {
372
await act(() => {
373
root.render(<div ref={true} />);
374
});
375
- }).rejects.toThrow(
376
- 'Element ref was specified as a string (true) but no owner was set.',
377
- );
375
+ // TODO: This throws an AggregateError. Need to update test infra to
376
+ // support matching against AggregateError.
377
+ }).rejects.toThrow();
378
await expect(async () => {
379
await act(() => {
380
root.render(<div ref={Symbol('foo')} />);
@@ -520,14 +520,10 @@ describe('creating element with string ref in constructor', () => {
520
await act(() => {
521
root.render(<RefTest />);
522
});
523
- }).rejects.toThrowError(
524
- 'Element ref was specified as a string (p) but no owner was set. This could happen for one of' +
525
- ' the following reasons:\n' +
526
- '1. You may be adding a ref to a function component\n' +
527
- "2. You may be adding a ref to a component that was not created inside a component's render method\n" +
528
- '3. You have multiple copies of React loaded\n' +
529
- 'See https://react.dev/link/refs-must-have-owner for more information.',
530
- );
523
+ })
524
+ // TODO: This throws an AggregateError. Need to update test infra to
525
+ // support matching against AggregateError.
526
+ .rejects.toThrowError();
527
});
528
});
529
@@ -581,10 +577,11 @@ describe('strings refs across renderers', () => {
577
);
578
});
579
}).toErrorDev([
584
- 'Warning: Component "Indirection" contains the string ref "child1". ' +
580
+ 'Warning: Component "Parent" contains the string ref "child1". ' +
581
'Support for string refs will be removed in a future major release. ' +
582
'We recommend using useRef() or createRef() instead. ' +
583
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
584
+ ' in div (at **)\n' +
585
' in Indirection (at **)\n' +
586
' in Parent (at **)',
587
]);
@@ -593,20 +590,10 @@ describe('strings refs across renderers', () => {
590
expect(inst.refs.child1.tagName).toBe('DIV');
591
expect(inst.refs.child1).toBe(div1.firstChild);
592
596
- await expect(async () => {
597
- // Now both refs should be rendered.
598
- await act(() => {
599
- root.render(<Parent />);
600
- });
601
- }).toErrorDev(
602
- [
603
- 'Warning: Component "Root" contains the string ref "child2". ' +
604
- 'Support for string refs will be removed in a future major release. ' +
605
- 'We recommend using useRef() or createRef() instead. ' +
606
- 'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref',
607
- ],
608
- {withoutStack: true},
609
- );
593
+ // Now both refs should be rendered.
594
+ await act(() => {
595
+ root.render(<Parent />);
596
+ });
597
expect(inst.refs.child1.tagName).toBe('DIV');
598
expect(inst.refs.child1).toBe(div1.firstChild);
599
expect(inst.refs.child2.tagName).toBe('DIV');
packages/react-reconciler/src/ReactChildFiber.js
+6
-150
@@ -33,17 +33,9 @@ import {
33
REACT_LAZY_TYPE,
34
REACT_CONTEXT_TYPE,
35
} from 'shared/ReactSymbols';
36
-import {
37
- ClassComponent,
38
- HostRoot,
39
- HostText,
40
- HostPortal,
41
- Fragment,
42
-} from './ReactWorkTags';
36
+import {HostRoot, HostText, HostPortal, Fragment} from './ReactWorkTags';
37
import isArray from 'shared/isArray';
44
-import assign from 'shared/assign';
45
-import {checkPropStringCoercion} from 'shared/CheckStringCoercion';
46
-import {enableRefAsProp, disableStringRefs} from 'shared/ReactFeatureFlags';
38
+import {enableRefAsProp} from 'shared/ReactFeatureFlags';
39
40
import {
41
createWorkInProgress,
@@ -84,7 +76,6 @@ function mergeDebugInfo(
76
77
let didWarnAboutMaps;
78
let didWarnAboutGenerators;
87
-let didWarnAboutStringRefs;
79
let ownerHasKeyUseWarning;
80
let ownerHasFunctionTypeWarning;
81
let ownerHasSymbolTypeWarning;
@@ -93,7 +84,6 @@ let warnForMissingKey = (child: mixed, returnFiber: Fiber) => {};
84
if (__DEV__) {
85
didWarnAboutMaps = false;
86
didWarnAboutGenerators = false;
96
- didWarnAboutStringRefs = ({}: {[string]: boolean});
87
88
/**
89
* Warn if there's no key explicitly set on dynamic arrays of children or
@@ -137,10 +127,6 @@ if (__DEV__) {
127
};
128
}
129
140
-function isReactClass(type: any) {
141
- return type.prototype && type.prototype.isReactComponent;
142
-}
143
-
130
function unwrapThenable<T>(thenable: Thenable<T>): T {
131
const index = thenableIndexCounter;
132
thenableIndexCounter += 1;
@@ -150,157 +136,27 @@ function unwrapThenable<T>(thenable: Thenable<T>): T {
136
return trackUsedThenable(thenableState, thenable, index);
137
}
138
153
-type CoercedStringRef = ((handle: mixed) => void) & {_stringRef: ?string, ...};
154
-
155
-function convertStringRefToCallbackRef(
156
- returnFiber: Fiber,
157
- current: Fiber | null,
158
- element: ReactElement,
159
- mixedRef: string | number | boolean,
160
-): CoercedStringRef {
161
- if (__DEV__) {
162
- checkPropStringCoercion(mixedRef, 'ref');
163
- }
164
- const stringRef = '' + (mixedRef: any);
165
-
166
- const owner: ?Fiber = (element._owner: any);
167
- if (!owner) {
168
- throw new Error(
169
- `Element ref was specified as a string (${stringRef}) 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://react.dev/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://react.dev/link/strict-mode-string-ref',
183
- );
184
- }
185
-
186
- if (__DEV__) {
187
- if (
188
- // Will already warn with "Function components cannot be given refs"
189
- !(typeof element.type === 'function' && !isReactClass(element.type))
190
- ) {
191
- const componentName =
192
- getComponentNameFromFiber(returnFiber) || 'Component';
193
- if (!didWarnAboutStringRefs[componentName]) {
194
- console.error(
195
- 'Component "%s" contains the string ref "%s". Support for string refs ' +
196
- 'will be removed in a future major release. We recommend using ' +
197
- 'useRef() or createRef() instead. ' +
198
- 'Learn more about using refs safely here: ' +
199
- 'https://react.dev/link/strict-mode-string-ref',
200
- componentName,
201
- stringRef,
202
- );
203
- didWarnAboutStringRefs[componentName] = true;
204
- }
205
- }
206
- }
207
-
208
- const inst = owner.stateNode;
209
- if (!inst) {
210
- throw new Error(
211
- `Missing owner for string ref ${stringRef}. This error is likely caused by a ` +
212
- 'bug in React. Please file an issue.',
213
- );
214
- }
215
-
216
- // Check if previous string ref matches new string ref
217
- if (
218
- current !== null &&
219
- current.ref !== null &&
220
- typeof current.ref === 'function' &&
221
- current.ref._stringRef === stringRef
222
- ) {
223
- // Reuse the existing string ref
224
- const currentRef: CoercedStringRef = ((current.ref: any): CoercedStringRef);
225
- return currentRef;
226
- }
227
-
228
- // Create a new string ref
229
- const ref = function (value: mixed) {
230
- const refs = inst.refs;
231
- if (value === null) {
232
- delete refs[stringRef];
233
- } else {
234
- refs[stringRef] = value;
235
- }
236
- };
237
- ref._stringRef = stringRef;
238
- return ref;
239
-}
240
-
139
function coerceRef(
140
returnFiber: Fiber,
141
current: Fiber | null,
142
workInProgress: Fiber,
143
element: ReactElement,
144
): void {
247
- let mixedRef;
145
+ let ref;
146
if (enableRefAsProp) {
147
// TODO: This is a temporary, intermediate step. When enableRefAsProp is on,
148
// we should resolve the `ref` prop during the begin phase of the component
149
// it's attached to (HostComponent, ClassComponent, etc).
150
const refProp = element.props.ref;
253
- mixedRef = refProp !== undefined ? refProp : null;
151
+ ref = refProp !== undefined ? refProp : null;
152
} else {
153
// Old behavior.
256
- mixedRef = element.ref;
257
- }
258
-
259
- let coercedRef;
260
- if (
261
- !disableStringRefs &&
262
- (typeof mixedRef === 'string' ||
263
- typeof mixedRef === 'number' ||
264
- typeof mixedRef === 'boolean')
265
- ) {
266
- coercedRef = convertStringRefToCallbackRef(
267
- returnFiber,
268
- current,
269
- element,
270
- mixedRef,
271
- );
272
-
273
- if (enableRefAsProp) {
274
- // When enableRefAsProp is on, we should always use the props as the
275
- // source of truth for refs. Not a field on the fiber.
276
- //
277
- // In the case of string refs, this presents a problem, because string
278
- // refs are not passed around internally as strings; they are converted to
279
- // callback refs. The ref used by the reconciler is not the same as the
280
- // one the user provided.
281
- //
282
- // But since this is a deprecated feature anyway, what we can do is clone
283
- // the props object and replace it with the internal callback ref. Then we
284
- // can continue to use the props object as the source of truth.
285
- //
286
- // This means the internal callback ref will leak into userspace. The
287
- // receiving component will receive a callback ref even though the parent
288
- // passed a string. Which is weird, but again, this is a deprecated
289
- // feature, and we're only leaving it around behind a flag so that Meta
290
- // can keep using string refs temporarily while they finish migrating
291
- // their codebase.
292
- const userProvidedProps = workInProgress.pendingProps;
293
- const propsWithInternalCallbackRef = assign({}, userProvidedProps);
294
- propsWithInternalCallbackRef.ref = coercedRef;
295
- workInProgress.pendingProps = propsWithInternalCallbackRef;
296
- }
297
- } else {
298
- coercedRef = mixedRef;
154
+ ref = element.ref;
155
}
156
157
// TODO: If enableRefAsProp is on, we shouldn't use the `ref` field. We
158
// should always read the ref from the prop.
303
- workInProgress.ref = coercedRef;
159
+ workInProgress.ref = ref;
160
}
161
162
function throwOnInvalidObjectType(returnFiber: Fiber, newChild: Object) {
packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.js
+1
@@ -1374,6 +1374,7 @@ describe('ReactIncrementalSideEffects', () => {
1374
'Support for string refs will be removed in a future major release. ' +
1375
'We recommend using useRef() or createRef() instead. ' +
1376
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
1377
+ ' in Bar (at **)\n' +
1378
' in Foo (at **)',
1379
]);
1380
expect(fooInstance.refs.bar.test).toEqual('test');
packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee
+1
-1
@@ -558,7 +558,7 @@ describe 'ReactCoffeeScriptClass', ->
558
'Support for string refs will be removed in a future major release. ' +
559
'We recommend using useRef() or createRef() instead. ' +
560
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
561
- ' in Foo (at **)'
561
+ ' in _Class (at **)'
562
]);
563
expect(ref.current.refs.inner.getName()).toBe 'foo'
564
packages/react/src/__tests__/ReactES6Class-test.js
+1
-1
@@ -602,7 +602,7 @@ describe('ReactES6Class', () => {
602
'Support for string refs will be removed in a future major release. ' +
603
'We recommend using useRef() or createRef() instead. ' +
604
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
605
- ' in Foo (at **)',
605
+ ' in Inner (at **)',
606
]);
607
expect(ref.current.refs.inner.getName()).toBe('foo');
608
});
packages/react/src/__tests__/ReactElementClone-test.js
+7
-3
@@ -359,16 +359,20 @@ describe('ReactElementClone', () => {
359
const clone = React.cloneElement(element, props);
360
expect(clone.type).toBe(ComponentClass);
361
expect(clone.key).toBe('12');
362
- if (gate(flags => flags.enableRefAsProp)) {
362
+ if (gate(flags => flags.enableRefAsProp && flags.disableStringRefs)) {
363
expect(clone.props.ref).toBe('34');
364
expect(() => expect(clone.ref).toBe('34')).toErrorDev(
365
'Accessing element.ref was removed in React 19',
366
{withoutStack: true},
367
);
368
expect(clone.props).toEqual({foo: 'ef', ref: '34'});
369
- } else {
370
- expect(clone.ref).toBe('34');
369
+ } else if (
370
+ gate(flags => !flags.enableRefAsProp && !flags.disableStringRefs)
371
+ ) {
372
+ expect(clone.ref).toBe(element.ref);
373
expect(clone.props).toEqual({foo: 'ef'});
374
+ } else {
375
+ // Not going to bother testing every possible combination.
376
}
377
if (__DEV__) {
378
expect(Object.isFrozen(element)).toBe(true);
packages/react/src/__tests__/ReactStrictMode-test.js
+4
-4
@@ -997,11 +997,11 @@ describe('string refs', () => {
997
root.render(<OuterComponent />);
998
});
999
}).toErrorDev(
1000
- 'Warning: Component "StrictMode" contains the string ref "somestring". ' +
1000
+ 'Warning: Component "OuterComponent" contains the string ref "somestring". ' +
1001
'Support for string refs will be removed in a future major release. ' +
1002
'We recommend using useRef() or createRef() instead. ' +
1003
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
1004
- ' in OuterComponent (at **)',
1004
+ ' in InnerComponent (at **)',
1005
);
1006
1007
await act(() => {
@@ -1036,11 +1036,11 @@ describe('string refs', () => {
1036
root.render(<OuterComponent />);
1037
});
1038
}).toErrorDev(
1039
- 'Warning: Component "StrictMode" contains the string ref "somestring". ' +
1039
+ 'Warning: Component "OuterComponent" contains the string ref "somestring". ' +
1040
'Support for string refs will be removed in a future major release. ' +
1041
'We recommend using useRef() or createRef() instead. ' +
1042
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
1043
- ' in OuterComponent (at **)',
1043
+ ' in InnerComponent (at **)',
1044
);
1045
1046
await act(() => {
packages/react/src/__tests__/ReactTypeScriptClass-test.ts
+1
-1
@@ -704,7 +704,7 @@ describe('ReactTypeScriptClass', function() {
704
'Support for string refs will be removed in a future major release. ' +
705
'We recommend using useRef() or createRef() instead. ' +
706
'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' +
707
- ' in ClassicRefs (at **)',
707
+ ' in Inner (at **)',
708
]);
709
expect(ref.current.refs.inner.getName()).toBe('foo');
710
});
packages/react/src/jsx/ReactJSXElement.js
+133
-4
@@ -23,6 +23,9 @@ import {
23
disableStringRefs,
24
disableDefaultPropsExceptForClasses,
25
} from 'shared/ReactFeatureFlags';
26
+import {checkPropStringCoercion} from 'shared/CheckStringCoercion';
27
+import {ClassComponent} from 'react-reconciler/src/ReactWorkTags';
28
+import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
29
30
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
31
const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
@@ -342,6 +345,9 @@ export function jsxProd(type, config, maybeKey) {
345
if (hasValidRef(config)) {
346
if (!enableRefAsProp) {
347
ref = config.ref;
348
+ if (!disableStringRefs) {
349
+ ref = coerceStringRef(ref, ReactCurrentOwner.current, type);
350
+ }
351
}
352
}
353
@@ -353,7 +359,15 @@ export function jsxProd(type, config, maybeKey) {
359
propName !== 'key' &&
360
(enableRefAsProp || propName !== 'ref')
361
) {
356
- props[propName] = config[propName];
362
+ if (enableRefAsProp && !disableStringRefs && propName === 'ref') {
363
+ props.ref = coerceStringRef(
364
+ config[propName],
365
+ ReactCurrentOwner.current,
366
+ type,
367
+ );
368
+ } else {
369
+ props[propName] = config[propName];
370
+ }
371
}
372
}
373
@@ -555,6 +569,9 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) {
569
if (hasValidRef(config)) {
570
if (!enableRefAsProp) {
571
ref = config.ref;
572
+ if (!disableStringRefs) {
573
+ ref = coerceStringRef(ref, ReactCurrentOwner.current, type);
574
+ }
575
}
576
if (!disableStringRefs) {
577
warnIfStringRefCannotBeAutoConverted(config, self);
@@ -569,7 +586,15 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) {
586
propName !== 'key' &&
587
(enableRefAsProp || propName !== 'ref')
588
) {
572
- props[propName] = config[propName];
589
+ if (enableRefAsProp && !disableStringRefs && propName === 'ref') {
590
+ props.ref = coerceStringRef(
591
+ config[propName],
592
+ ReactCurrentOwner.current,
593
+ type,
594
+ );
595
+ } else {
596
+ props[propName] = config[propName];
597
+ }
598
}
599
}
600
@@ -687,6 +712,9 @@ export function createElement(type, config, children) {
712
if (hasValidRef(config)) {
713
if (!enableRefAsProp) {
714
ref = config.ref;
715
+ if (!disableStringRefs) {
716
+ ref = coerceStringRef(ref, ReactCurrentOwner.current, type);
717
+ }
718
}
719
720
if (__DEV__ && !disableStringRefs) {
@@ -714,7 +742,15 @@ export function createElement(type, config, children) {
742
propName !== '__self' &&
743
propName !== '__source'
744
) {
717
- props[propName] = config[propName];
745
+ if (enableRefAsProp && !disableStringRefs && propName === 'ref') {
746
+ props.ref = coerceStringRef(
747
+ config[propName],
748
+ ReactCurrentOwner.current,
749
+ type,
750
+ );
751
+ } else {
752
+ props[propName] = config[propName];
753
+ }
754
}
755
}
756
}
@@ -820,6 +856,9 @@ export function cloneElement(element, config, children) {
856
if (!enableRefAsProp) {
857
// Silently steal the ref from the parent.
858
ref = config.ref;
859
+ if (!disableStringRefs) {
860
+ ref = coerceStringRef(ref, owner, element.type);
861
+ }
862
}
863
owner = ReactCurrentOwner.current;
864
}
@@ -866,7 +905,11 @@ export function cloneElement(element, config, children) {
905
// Resolve default props
906
props[propName] = defaultProps[propName];
907
} else {
869
- props[propName] = config[propName];
908
+ if (enableRefAsProp && !disableStringRefs && propName === 'ref') {
909
+ props.ref = coerceStringRef(config[propName], owner, element.type);
910
+ } else {
911
+ props[propName] = config[propName];
912
+ }
913
}
914
}
915
}
@@ -1086,3 +1129,89 @@ function validateFragmentProps(fragment) {
1129
}
1130
}
1131
}
1132
+
1133
+function coerceStringRef(mixedRef, owner, type) {
1134
+ if (disableStringRefs) {
1135
+ return mixedRef;
1136
+ }
1137
+
1138
+ let stringRef;
1139
+ if (typeof mixedRef === 'string') {
1140
+ stringRef = mixedRef;
1141
+ } else {
1142
+ if (typeof mixedRef === 'number' || typeof mixedRef === 'boolean') {
1143
+ if (__DEV__) {
1144
+ checkPropStringCoercion(mixedRef, 'ref');
1145
+ }
1146
+ stringRef = '' + mixedRef;
1147
+ } else {
1148
+ return mixedRef;
1149
+ }
1150
+ }
1151
+
1152
+ return stringRefAsCallbackRef.bind(null, stringRef, type, owner);
1153
+}
1154
+
1155
+function stringRefAsCallbackRef(stringRef, type, owner, value) {
1156
+ if (disableStringRefs) {
1157
+ return;
1158
+ }
1159
+ if (!owner) {
1160
+ throw new Error(
1161
+ `Element ref was specified as a string (${stringRef}) but no owner was set. This could happen for one of` +
1162
+ ' the following reasons:\n' +
1163
+ '1. You may be adding a ref to a function component\n' +
1164
+ "2. You may be adding a ref to a component that was not created inside a component's render method\n" +
1165
+ '3. You have multiple copies of React loaded\n' +
1166
+ 'See https://react.dev/link/refs-must-have-owner for more information.',
1167
+ );
1168
+ }
1169
+ if (owner.tag !== ClassComponent) {
1170
+ throw new Error(
1171
+ 'Function components cannot have string refs. ' +
1172
+ 'We recommend using useRef() instead. ' +
1173
+ 'Learn more about using refs safely here: ' +
1174
+ 'https://react.dev/link/strict-mode-string-ref',
1175
+ );
1176
+ }
1177
+
1178
+ if (__DEV__) {
1179
+ if (
1180
+ // Will already warn with "Function components cannot be given refs"
1181
+ !(typeof type === 'function' && !isReactClass(type))
1182
+ ) {
1183
+ const componentName = getComponentNameFromFiber(owner) || 'Component';
1184
+ if (!didWarnAboutStringRefs[componentName]) {
1185
+ console.error(
1186
+ 'Component "%s" contains the string ref "%s". Support for string refs ' +
1187
+ 'will be removed in a future major release. We recommend using ' +
1188
+ 'useRef() or createRef() instead. ' +
1189
+ 'Learn more about using refs safely here: ' +
1190
+ 'https://react.dev/link/strict-mode-string-ref',
1191
+ componentName,
1192
+ stringRef,
1193
+ );
1194
+ didWarnAboutStringRefs[componentName] = true;
1195
+ }
1196
+ }
1197
+ }
1198
+
1199
+ const inst = owner.stateNode;
1200
+ if (!inst) {
1201
+ throw new Error(
1202
+ `Missing owner for string ref ${stringRef}. This error is likely caused by a ` +
1203
+ 'bug in React. Please file an issue.',
1204
+ );
1205
+ }
1206
+
1207
+ const refs = inst.refs;
1208
+ if (value === null) {
1209
+ delete refs[stringRef];
1210
+ } else {
1211
+ refs[stringRef] = value;
1212
+ }
1213
+}
1214
+
1215
+function isReactClass(type) {
1216
+ return type.prototype && type.prototype.isReactComponent;
1217
+}