@samitouri / QOS-React-1 / commits / 2f8f776022

Move ref type check to receiver (#28464)

The runtime contains a type check to determine if a user-provided ref is a valid type — a function or object (or a string, when `disableStringRefs` is off). This currently happens during child reconciliation. This changes it to happen only when the ref is passed to the component that the ref is being attached to. This is a continuation of the "ref as prop" change — until you actually pass a ref to a HostComponent, class, etc, ref is a normal prop that has no special behavior.

Andrew Clark committed Feb 29, 2024 at 21:26 UTC 2f8f7760223241665f472a2a9be16650473bce39
6 files changed +44 -43
packages/react-dom/src/__tests__/ReactComponent-test.js
+5 -2
@@ -38,7 +38,7 @@ describe('ReactComponent', () => {
38 }).toThrowError(/Target container is not a DOM element./);
39 });
40
41 - // @gate !disableStringRefs || !__DEV__
41 + // @gate !disableStringRefs
42 it('should throw when supplying a string ref outside of render method', async () => {
43 const container = document.createElement('div');
44 const root = ReactDOMClient.createRoot(container);
@@ -46,7 +46,10 @@ describe('ReactComponent', () => {
46 act(() => {
47 root.render(<div ref="badDiv" />);
48 }),
49 - ).rejects.toThrow();
49 + ).rejects.toThrow(
50 + 'Element ref was specified as a string (badDiv) but no owner ' +
51 + 'was set',
52 + );
53 });
54
55 it('should throw (in dev) when children are mutated during render', async () => {
packages/react-dom/src/__tests__/refs-test.js
+4 -8
@@ -401,22 +401,20 @@ describe('ref swapping', () => {
401 root.render(<div ref={10} />);
402 });
403 }).rejects.toThrow(
404 - 'Expected ref to be a function, a string, an object returned by React.createRef(), or null.',
404 + 'Element ref was specified as a string (10) but no owner was set.',
405 );
406 await expect(async () => {
407 await act(() => {
408 root.render(<div ref={true} />);
409 });
410 }).rejects.toThrow(
411 - 'Expected ref to be a function, a string, an object returned by React.createRef(), or null.',
411 + 'Element ref was specified as a string (true) but no owner was set.',
412 );
413 await expect(async () => {
414 await act(() => {
415 root.render(<div ref={Symbol('foo')} />);
416 });
417 - }).rejects.toThrow(
418 - 'Expected ref to be a function, a string, an object returned by React.createRef(), or null.',
419 - );
417 + }).rejects.toThrow('Expected ref to be a function');
418 });
419
420 // @gate !enableRefAsProp
@@ -434,9 +432,7 @@ describe('ref swapping', () => {
432 key: null,
433 });
434 });
437 - }).rejects.toThrow(
438 - 'Expected ref to be a function, a string, an object returned by React.createRef(), or null.',
439 - );
435 + }).rejects.toThrow('Expected ref to be a function');
436 });
437 });
438
packages/react-reconciler/src/ReactChildFiber.js
+10 -19
@@ -157,17 +157,17 @@ function convertStringRefToCallbackRef(
157 returnFiber: Fiber,
158 current: Fiber | null,
159 element: ReactElement,
160 - mixedRef: any,
160 + mixedRef: string | number | boolean,
161 ): CoercedStringRef {
162 + if (__DEV__) {
163 + checkPropStringCoercion(mixedRef, 'ref');
164 + }
165 + const stringRef = '' + (mixedRef: any);
166 +
167 const owner: ?Fiber = (element._owner: any);
168 if (!owner) {
164 - if (typeof mixedRef !== 'string') {
165 - throw new Error(
166 - 'Expected ref to be a function, a string, an object returned by React.createRef(), or null.',
167 - );
168 - }
169 throw new Error(
170 - `Element ref was specified as a string (${mixedRef}) but no owner was set. This could happen for one of` +
170 + `Element ref was specified as a string (${stringRef}) but no owner was set. This could happen for one of` +
171 ' the following reasons:\n' +
172 '1. You may be adding a ref to a function component\n' +
173 "2. You may be adding a ref to a component that was not created inside a component's render method\n" +
@@ -184,13 +184,6 @@ function convertStringRefToCallbackRef(
184 );
185 }
186
187 - // At this point, we know the ref isn't an object or function but it could
188 - // be a number. Coerce it to a string.
189 - if (__DEV__) {
190 - checkPropStringCoercion(mixedRef, 'ref');
191 - }
192 - const stringRef = '' + mixedRef;
193 -
187 if (__DEV__) {
188 if (
189 // Will already warn with "Function components cannot be given refs"
@@ -267,12 +260,10 @@ function coerceRef(
260 let coercedRef;
261 if (
262 !disableStringRefs &&
270 - mixedRef !== null &&
271 - typeof mixedRef !== 'function' &&
272 - typeof mixedRef !== 'object'
263 + (typeof mixedRef === 'string' ||
264 + typeof mixedRef === 'number' ||
265 + typeof mixedRef === 'boolean')
266 ) {
274 - // Assume this is a string ref. If it's not, then this will throw an error
275 - // to the user.
267 coercedRef = convertStringRefToCallbackRef(
268 returnFiber,
269 current,
packages/react-reconciler/src/ReactFiberBeginWork.js
+16 -9
@@ -1026,16 +1026,23 @@ function updateProfiler(
1026 }
1027
1028 function markRef(current: Fiber | null, workInProgress: Fiber) {
1029 - // TODO: This is also where we should check the type of the ref and error if
1030 - // an invalid one is passed, instead of during child reconcilation.
1029 + // TODO: Check props.ref instead of fiber.ref when enableRefAsProp is on.
1030 const ref = workInProgress.ref;
1032 - if (
1033 - (current === null && ref !== null) ||
1034 - (current !== null && current.ref !== ref)
1035 - ) {
1036 - // Schedule a Ref effect
1037 - workInProgress.flags |= Ref;
1038 - workInProgress.flags |= RefStatic;
1031 + if (ref === null) {
1032 + if (current !== null && current.ref !== null) {
1033 + // Schedule a Ref effect
1034 + workInProgress.flags |= Ref | RefStatic;
1035 + }
1036 + } else {
1037 + if (typeof ref !== 'function' && typeof ref !== 'object') {
1038 + throw new Error(
1039 + 'Expected ref to be a function, an object returned by React.createRef(), or undefined/null.',
1040 + );
1041 + }
1042 + if (current === null || current.ref !== ref) {
1043 + // Schedule a Ref effect
1044 + workInProgress.flags |= Ref | RefStatic;
1045 + }
1046 }
1047 }
1048
packages/react-reconciler/src/__tests__/ReactFiberRefs-test.js
+8 -4
@@ -115,9 +115,13 @@ describe('ReactFiberRefs', () => {
115 });
116
117 // @gate disableStringRefs
118 - test('log an error in dev if a string ref is passed to a ref-receiving component', async () => {
118 + test('throw if a string ref is passed to a ref-receiving component', async () => {
119 let refProp;
120 function Child({ref}) {
121 + // This component renders successfully because the ref type check does not
122 + // occur until you pass it to a component that accepts refs.
123 + //
124 + // So the div will throw, but not Child.
125 refProp = ref;
126 return <div ref={ref} />;
127 }
@@ -129,9 +133,9 @@ describe('ReactFiberRefs', () => {
133 }
134
135 const root = ReactNoop.createRoot();
132 - await expect(async () => {
133 - await expect(act(() => root.render(<Owner />))).rejects.toThrow();
134 - }).toErrorDev('String refs are no longer supported');
136 + await expect(act(() => root.render(<Owner />))).rejects.toThrow(
137 + 'Expected ref to be a function',
138 + );
139 expect(refProp).toBe('child');
140 });
141 });
scripts/error-codes/codes.json
+1 -1
@@ -280,7 +280,7 @@
280 "281": "Finished root should have a work-in-progress. This error is likely caused by a bug in React. Please file an issue.",
281 "282": "If the root does not have an updateQueue, we should have already bailed out. This error is likely caused by a bug in React. Please file an issue.",
282 "283": "Element type is invalid. Received a promise that resolves to: %s. Promise elements must resolve to a class or function.",
283 - "284": "Expected ref to be a function, a string, an object returned by React.createRef(), or null.",
283 + "284": "Expected ref to be a function, an object returned by React.createRef(), or undefined/null.",
284 "285": "The root failed to unmount after an error. This is likely a bug in React. Please file an issue.",
285 "286": "%s(...): the first argument must be a React class instance. Instead received: %s.",
286 "287": "It is not supported to run the profiling version of a renderer (for example, `react-dom/profiling`) without also replacing the `schedule/tracking` module with `schedule/tracking-profiling`. Your bundler might have a setting for aliasing both modules. Learn more at https://reactjs.org/link/profiling",