Skip to content

Commit 2f8f776

Browse files
authored
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.
1 parent bb4b147 commit 2f8f776

File tree

6 files changed

+44
-43
lines changed

6 files changed

+44
-43
lines changed

packages/react-dom/src/__tests__/ReactComponent-test.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,18 @@ describe('ReactComponent', () => {
3838
}).toThrowError(/Target container is not a DOM element./);
3939
});
4040

41-
// @gate !disableStringRefs || !__DEV__
41+
// @gate !disableStringRefs
4242
it('should throw when supplying a string ref outside of render method', async () => {
4343
const container = document.createElement('div');
4444
const root = ReactDOMClient.createRoot(container);
4545
await expect(
4646
act(() => {
4747
root.render(<div ref="badDiv" />);
4848
}),
49-
).rejects.toThrow();
49+
).rejects.toThrow(
50+
'Element ref was specified as a string (badDiv) but no owner ' +
51+
'was set',
52+
);
5053
});
5154

5255
it('should throw (in dev) when children are mutated during render', async () => {

packages/react-dom/src/__tests__/refs-test.js

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -401,22 +401,20 @@ describe('ref swapping', () => {
401401
root.render(<div ref={10} />);
402402
});
403403
}).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.',
405405
);
406406
await expect(async () => {
407407
await act(() => {
408408
root.render(<div ref={true} />);
409409
});
410410
}).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.',
412412
);
413413
await expect(async () => {
414414
await act(() => {
415415
root.render(<div ref={Symbol('foo')} />);
416416
});
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');
420418
});
421419

422420
// @gate !enableRefAsProp
@@ -434,9 +432,7 @@ describe('ref swapping', () => {
434432
key: null,
435433
});
436434
});
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');
440436
});
441437
});
442438

packages/react-reconciler/src/ReactChildFiber.js

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -157,17 +157,17 @@ function convertStringRefToCallbackRef(
157157
returnFiber: Fiber,
158158
current: Fiber | null,
159159
element: ReactElement,
160-
mixedRef: any,
160+
mixedRef: string | number | boolean,
161161
): CoercedStringRef {
162+
if (__DEV__) {
163+
checkPropStringCoercion(mixedRef, 'ref');
164+
}
165+
const stringRef = '' + (mixedRef: any);
166+
162167
const owner: ?Fiber = (element._owner: any);
163168
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-
}
169169
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` +
171171
' the following reasons:\n' +
172172
'1. You may be adding a ref to a function component\n' +
173173
"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(
184184
);
185185
}
186186

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-
194187
if (__DEV__) {
195188
if (
196189
// Will already warn with "Function components cannot be given refs"
@@ -267,12 +260,10 @@ function coerceRef(
267260
let coercedRef;
268261
if (
269262
!disableStringRefs &&
270-
mixedRef !== null &&
271-
typeof mixedRef !== 'function' &&
272-
typeof mixedRef !== 'object'
263+
(typeof mixedRef === 'string' ||
264+
typeof mixedRef === 'number' ||
265+
typeof mixedRef === 'boolean')
273266
) {
274-
// Assume this is a string ref. If it's not, then this will throw an error
275-
// to the user.
276267
coercedRef = convertStringRefToCallbackRef(
277268
returnFiber,
278269
current,

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,16 +1026,23 @@ function updateProfiler(
10261026
}
10271027

10281028
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.
10311030
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+
}
10391046
}
10401047
}
10411048

packages/react-reconciler/src/__tests__/ReactFiberRefs-test.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,13 @@ describe('ReactFiberRefs', () => {
115115
});
116116

117117
// @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 () => {
119119
let refProp;
120120
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.
121125
refProp = ref;
122126
return <div ref={ref} />;
123127
}
@@ -129,9 +133,9 @@ describe('ReactFiberRefs', () => {
129133
}
130134

131135
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+
);
135139
expect(refProp).toBe('child');
136140
});
137141
});

scripts/error-codes/codes.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@
280280
"281": "Finished root should have a work-in-progress. This error is likely caused by a bug in React. Please file an issue.",
281281
"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.",
282282
"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.",
284284
"285": "The root failed to unmount after an error. This is likely a bug in React. Please file an issue.",
285285
"286": "%s(...): the first argument must be a React class instance. Instead received: %s.",
286286
"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",

0 commit comments

Comments
 (0)