@samitouri / QOS-React / commits / e3ebcd54b9

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 +}