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",