@samitouri / QOS-React-1 / commits / fa2f82addc

Pass ref as normal prop (#28348)

Depends on: - #28317 - #28320 --- Changes the behavior of the JSX runtime to pass through `ref` as a normal prop, rather than plucking it from the props object and storing on the element. This is a breaking change since it changes the type of the receiving component. However, most code is unaffected since it's unlikely that a component would have attempted to access a `ref` prop, since it was not possible to get a reference to one. `forwardRef` _will_ still pluck `ref` from the props object, though, since it's extremely common for users to spread the props object onto the inner component and pass `ref` as a differently named prop. This is for maximum compatibility with existing code — the real impact of this change is that `forwardRef` is no longer required. Currently, refs are resolved during child reconciliation and stored on the fiber. As a result of this change, we can move ref resolution to happen only much later, and only for components that actually use them. Then we can remove the `ref` field from the Fiber type. I have not yet done that in this step, though.

Andrew Clark committed Feb 20, 2024 at 14:17 UTC fa2f82addc7c817892c482792f56a35277e8b75a
34 files changed +672 -243
packages/jest-react/src/JestReact.js
+38 -23
@@ -6,6 +6,7 @@
6 */
7
8 import {REACT_ELEMENT_TYPE, REACT_FRAGMENT_TYPE} from 'shared/ReactSymbols';
9 +import {enableRefAsProp} from 'shared/ReactFeatureFlags';
10
11 import isArray from 'shared/isArray';
12
@@ -38,6 +39,34 @@ function assertYieldsWereCleared(root) {
39 }
40 }
41
42 +function createJSXElementForTestComparison(type, props) {
43 + if (__DEV__ && enableRefAsProp) {
44 + const element = {
45 + $$typeof: REACT_ELEMENT_TYPE,
46 + type: type,
47 + key: null,
48 + props: props,
49 + _owner: null,
50 + _store: __DEV__ ? {} : undefined,
51 + };
52 + Object.defineProperty(element, 'ref', {
53 + enumerable: false,
54 + value: null,
55 + });
56 + return element;
57 + } else {
58 + return {
59 + $$typeof: REACT_ELEMENT_TYPE,
60 + type: type,
61 + key: null,
62 + ref: null,
63 + props: props,
64 + _owner: null,
65 + _store: __DEV__ ? {} : undefined,
66 + };
67 + }
68 +}
69 +
70 export function unstable_toMatchRenderedOutput(root, expectedJSX) {
71 assertYieldsWereCleared(root);
72 const actualJSON = root.toJSON();
@@ -55,17 +84,9 @@ export function unstable_toMatchRenderedOutput(root, expectedJSX) {
84 if (actualJSXChildren === null || typeof actualJSXChildren === 'string') {
85 actualJSX = actualJSXChildren;
86 } else {
58 - actualJSX = {
59 - $$typeof: REACT_ELEMENT_TYPE,
60 - type: REACT_FRAGMENT_TYPE,
61 - key: null,
62 - ref: null,
63 - props: {
64 - children: actualJSXChildren,
65 - },
66 - _owner: null,
67 - _store: __DEV__ ? {} : undefined,
68 - };
87 + actualJSX = createJSXElementForTestComparison(REACT_FRAGMENT_TYPE, {
88 + children: actualJSXChildren,
89 + });
90 }
91 }
92 } else {
@@ -82,18 +103,12 @@ function jsonChildToJSXChild(jsonChild) {
103 return jsonChild;
104 } else {
105 const jsxChildren = jsonChildrenToJSXChildren(jsonChild.children);
85 - return {
86 - $$typeof: REACT_ELEMENT_TYPE,
87 - type: jsonChild.type,
88 - key: null,
89 - ref: null,
90 - props:
91 - jsxChildren === null
92 - ? jsonChild.props
93 - : {...jsonChild.props, children: jsxChildren},
94 - _owner: null,
95 - _store: __DEV__ ? {} : undefined,
96 - };
106 + return createJSXElementForTestComparison(
107 + jsonChild.type,
108 + jsxChildren === null
109 + ? jsonChild.props
110 + : {...jsonChild.props, children: jsxChildren},
111 + );
112 }
113 }
114
packages/react-client/src/ReactFlightClient.js
+40 -14
@@ -35,7 +35,11 @@ import type {
35
36 import type {Postpone} from 'react/src/ReactPostpone';
37
38 -import {enableBinaryFlight, enablePostpone} from 'shared/ReactFeatureFlags';
38 +import {
39 + enableBinaryFlight,
40 + enablePostpone,
41 + enableRefAsProp,
42 +} from 'shared/ReactFeatureFlags';
43
44 import {
45 resolveClientReference,
@@ -463,24 +467,46 @@ export function reportGlobalError(response: Response, error: Error): void {
467 });
468 }
469
470 +function nullRefGetter() {
471 + if (__DEV__) {
472 + return null;
473 + }
474 +}
475 +
476 function createElement(
477 type: mixed,
478 key: mixed,
479 props: mixed,
480 ): React$Element<any> {
471 - const element: any = {
472 - // This tag allows us to uniquely identify this as a React Element
473 - $$typeof: REACT_ELEMENT_TYPE,
474 -
475 - // Built-in properties that belong on the element
476 - type: type,
477 - key: key,
478 - ref: null,
479 - props: props,
480 -
481 - // Record the component responsible for creating this element.
482 - _owner: null,
483 - };
481 + let element: any;
482 + if (__DEV__ && enableRefAsProp) {
483 + // `ref` is non-enumerable in dev
484 + element = ({
485 + $$typeof: REACT_ELEMENT_TYPE,
486 + type,
487 + key,
488 + props,
489 + _owner: null,
490 + }: any);
491 + Object.defineProperty(element, 'ref', {
492 + enumerable: false,
493 + get: nullRefGetter,
494 + });
495 + } else {
496 + element = ({
497 + // This tag allows us to uniquely identify this as a React Element
498 + $$typeof: REACT_ELEMENT_TYPE,
499 +
500 + type,
501 + key,
502 + ref: null,
503 + props,
504 +
505 + // Record the component responsible for creating this element.
506 + _owner: null,
507 + }: any);
508 + }
509 +
510 if (__DEV__) {
511 // We don't really need to add any of these but keeping them for good measure.
512 // Unfortunately, _store is enumerable in jest matchers so for equality to
packages/react-devtools-shared/src/__tests__/legacy/storeLegacy-v15-test.js
+43 -36
@@ -753,37 +753,43 @@ describe('Store (legacy)', () => {
753 `);
754 });
755
756 - it('should support expanding deep parts of the tree', () => {
757 - const Wrapper = ({forwardedRef}) => (
758 - <Nested depth={3} forwardedRef={forwardedRef} />
759 - );
760 - const Nested = ({depth, forwardedRef}) =>
761 - depth > 0 ? (
762 - <Nested depth={depth - 1} forwardedRef={forwardedRef} />
763 - ) : (
764 - <div ref={forwardedRef} />
756 + // TODO: These tests don't work when enableRefAsProp is on because the
757 + // JSX runtime that's injected into the test environment by the compiler
758 + // is not compatible with older versions of React. Need to configure the
759 + // the test environment in such a way that certain test modules like this
760 + // one can use an older transform.
761 + if (!require('shared/ReactFeatureFlags').enableRefAsProp) {
762 + it('should support expanding deep parts of the tree', () => {
763 + const Wrapper = ({forwardedRef}) => (
764 + <Nested depth={3} forwardedRef={forwardedRef} />
765 );
766 -
767 - let ref = null;
768 - const refSetter = value => {
769 - ref = value;
770 - };
771 -
772 - act(() =>
773 - ReactDOM.render(
774 - <Wrapper forwardedRef={refSetter} />,
775 - document.createElement('div'),
776 - ),
777 - );
778 - expect(store).toMatchInlineSnapshot(`
766 + const Nested = ({depth, forwardedRef}) =>
767 + depth > 0 ? (
768 + <Nested depth={depth - 1} forwardedRef={forwardedRef} />
769 + ) : (
770 + <div ref={forwardedRef} />
771 + );
772 +
773 + let ref = null;
774 + const refSetter = value => {
775 + ref = value;
776 + };
777 +
778 + act(() =>
779 + ReactDOM.render(
780 + <Wrapper forwardedRef={refSetter} />,
781 + document.createElement('div'),
782 + ),
783 + );
784 + expect(store).toMatchInlineSnapshot(`
785 [root]
786 ▸ <Wrapper>
787 `);
788
783 - const deepestedNodeID = global.agent.getIDForNode(ref);
789 + const deepestedNodeID = global.agent.getIDForNode(ref);
790
785 - act(() => store.toggleIsCollapsed(deepestedNodeID, false));
786 - expect(store).toMatchInlineSnapshot(`
791 + act(() => store.toggleIsCollapsed(deepestedNodeID, false));
792 + expect(store).toMatchInlineSnapshot(`
793 [root]
794 ▾ <Wrapper>
795 ▾ <Nested>
@@ -793,16 +799,16 @@ describe('Store (legacy)', () => {
799 <div>
800 `);
801
796 - const rootID = store.getElementIDAtIndex(0);
802 + const rootID = store.getElementIDAtIndex(0);
803
798 - act(() => store.toggleIsCollapsed(rootID, true));
799 - expect(store).toMatchInlineSnapshot(`
804 + act(() => store.toggleIsCollapsed(rootID, true));
805 + expect(store).toMatchInlineSnapshot(`
806 [root]
807 ▸ <Wrapper>
808 `);
809
804 - act(() => store.toggleIsCollapsed(rootID, false));
805 - expect(store).toMatchInlineSnapshot(`
810 + act(() => store.toggleIsCollapsed(rootID, false));
811 + expect(store).toMatchInlineSnapshot(`
812 [root]
813 ▾ <Wrapper>
814 ▾ <Nested>
@@ -812,17 +818,17 @@ describe('Store (legacy)', () => {
818 <div>
819 `);
820
815 - const id = store.getElementIDAtIndex(1);
821 + const id = store.getElementIDAtIndex(1);
822
817 - act(() => store.toggleIsCollapsed(id, true));
818 - expect(store).toMatchInlineSnapshot(`
823 + act(() => store.toggleIsCollapsed(id, true));
824 + expect(store).toMatchInlineSnapshot(`
825 [root]
826 ▾ <Wrapper>
827 ▸ <Nested>
828 `);
829
824 - act(() => store.toggleIsCollapsed(id, false));
825 - expect(store).toMatchInlineSnapshot(`
830 + act(() => store.toggleIsCollapsed(id, false));
831 + expect(store).toMatchInlineSnapshot(`
832 [root]
833 ▾ <Wrapper>
834 ▾ <Nested>
@@ -831,7 +837,8 @@ describe('Store (legacy)', () => {
837 ▾ <Nested>
838 <div>
839 `);
834 - });
840 + });
841 + }
842
843 it('should support reordering of children', () => {
844 const Root = ({children}) => <div>{children}</div>;
packages/react-dom-bindings/src/client/ReactDOMComponent.js
+7 -2
@@ -640,7 +640,9 @@ function setProp(
640 case 'suppressHydrationWarning':
641 case 'defaultValue': // Reserved
642 case 'defaultChecked':
643 - case 'innerHTML': {
643 + case 'innerHTML':
644 + case 'ref': {
645 + // TODO: `ref` is pretty common, should we move it up?
646 // Noop
647 break;
648 }
@@ -988,7 +990,8 @@ function setPropOnCustomElement(
990 }
991 case 'suppressContentEditableWarning':
992 case 'suppressHydrationWarning':
991 - case 'innerHTML': {
993 + case 'innerHTML':
994 + case 'ref': {
995 // Noop
996 break;
997 }
@@ -2194,6 +2197,7 @@ function diffHydratedCustomComponent(
2197 case 'defaultValue':
2198 case 'defaultChecked':
2199 case 'innerHTML':
2200 + case 'ref':
2201 // Noop
2202 continue;
2203 case 'dangerouslySetInnerHTML':
@@ -2307,6 +2311,7 @@ function diffHydratedGenericElement(
2311 case 'defaultValue':
2312 case 'defaultChecked':
2313 case 'innerHTML':
2314 + case 'ref':
2315 // Noop
2316 continue;
2317 case 'dangerouslySetInnerHTML':
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+4
@@ -1226,6 +1226,7 @@ function pushAttribute(
1226 case 'innerHTML': // Must use dangerouslySetInnerHTML instead.
1227 case 'suppressContentEditableWarning':
1228 case 'suppressHydrationWarning':
1229 + case 'ref':
1230 // Ignored. These are built-in to React on the client.
1231 return;
1232 case 'autoFocus':
@@ -3391,6 +3392,7 @@ function pushStartCustomElement(
3392 break;
3393 case 'suppressContentEditableWarning':
3394 case 'suppressHydrationWarning':
3395 + case 'ref':
3396 // Ignored. These are built-in to React on the client.
3397 break;
3398 case 'className':
@@ -4964,6 +4966,7 @@ function writeStyleResourceAttributeInJS(
4966 case 'suppressContentEditableWarning':
4967 case 'suppressHydrationWarning':
4968 case 'style':
4969 + case 'ref':
4970 // Ignored
4971 return;
4972
@@ -5157,6 +5160,7 @@ function writeStyleResourceAttributeInAttr(
5160 case 'suppressContentEditableWarning':
5161 case 'suppressHydrationWarning':
5162 case 'style':
5163 + case 'ref':
5164 // Ignored
5165 return;
5166
packages/react-dom-bindings/src/shared/ReactDOMUnknownPropertyHook.js
+2 -1
@@ -186,7 +186,8 @@ function validateProperty(tagName, name, value, eventRegistry) {
186 case 'suppressHydrationWarning':
187 case 'defaultValue': // Reserved
188 case 'defaultChecked':
189 - case 'innerHTML': {
189 + case 'innerHTML':
190 + case 'ref': {
191 return true;
192 }
193 case 'innerText': // Properties
packages/react-dom/src/__tests__/ReactCompositeComponent-test.js
+30 -8
@@ -278,26 +278,48 @@ describe('ReactCompositeComponent', () => {
278 }
279 }
280
281 + function refFn1(ref) {
282 + instance1 = ref;
283 + }
284 +
285 + function refFn2(ref) {
286 + instance2 = ref;
287 + }
288 +
289 + function refFn3(ref) {
290 + instance3 = ref;
291 + }
292 +
293 let instance1;
294 let instance2;
295 let instance3;
296 const root = ReactDOMClient.createRoot(document.createElement('div'));
297 await act(() => {
286 - root.render(<Component ref={ref => (instance1 = ref)} />);
298 + root.render(<Component ref={refFn1} />);
299 });
288 - expect(instance1.props).toEqual({prop: 'testKey'});
300 + if (gate(flags => flags.enableRefAsProp)) {
301 + expect(instance1.props).toEqual({prop: 'testKey', ref: refFn1});
302 + } else {
303 + expect(instance1.props).toEqual({prop: 'testKey'});
304 + }
305
306 await act(() => {
291 - root.render(
292 - <Component ref={ref => (instance2 = ref)} prop={undefined} />,
293 - );
307 + root.render(<Component ref={refFn2} prop={undefined} />);
308 });
295 - expect(instance2.props).toEqual({prop: 'testKey'});
309 + if (gate(flags => flags.enableRefAsProp)) {
310 + expect(instance2.props).toEqual({prop: 'testKey', ref: refFn2});
311 + } else {
312 + expect(instance2.props).toEqual({prop: 'testKey'});
313 + }
314
315 await act(() => {
298 - root.render(<Component ref={ref => (instance3 = ref)} prop={null} />);
316 + root.render(<Component ref={refFn3} prop={null} />);
317 });
300 - expect(instance3.props).toEqual({prop: null});
318 + if (gate(flags => flags.enableRefAsProp)) {
319 + expect(instance3.props).toEqual({prop: null, ref: refFn3});
320 + } else {
321 + expect(instance3.props).toEqual({prop: null});
322 + }
323 });
324
325 it('should not mutate passed-in props object', async () => {
packages/react-dom/src/__tests__/ReactFunctionComponent-test.js
+5 -1
@@ -199,6 +199,7 @@ describe('ReactFunctionComponent', () => {
199 );
200 });
201
202 + // @gate !enableRefAsProp || !__DEV__
203 it('should warn when given a string ref', async () => {
204 function Indirection(props) {
205 return <div>{props.children}</div>;
@@ -240,7 +241,8 @@ describe('ReactFunctionComponent', () => {
241 });
242 });
243
243 - it('should warn when given a function ref and ignore them', async () => {
244 + // @gate !enableRefAsProp || !__DEV__
245 + it('should warn when given a function ref', async () => {
246 function Indirection(props) {
247 return <div>{props.children}</div>;
248 }
@@ -283,6 +285,7 @@ describe('ReactFunctionComponent', () => {
285 });
286 });
287
288 + // @gate !enableRefAsProp || !__DEV__
289 it('deduplicates ref warnings based on element or owner', async () => {
290 // When owner uses JSX, we can use exact line location to dedupe warnings
291 class AnonymousParentUsingJSX extends React.Component {
@@ -373,6 +376,7 @@ describe('ReactFunctionComponent', () => {
376 // This guards against a regression caused by clearing the current debug fiber.
377 // https://github.com/facebook/react/issues/10831
378 // @gate !disableLegacyContext || !__DEV__
379 + // @gate !enableRefAsProp || !__DEV__
380 it('should warn when giving a function ref with context', async () => {
381 function Child() {
382 return null;
packages/react-dom/src/__tests__/refs-test.js
+8 -17
@@ -414,30 +414,21 @@ describe('ref swapping', () => {
414 }).rejects.toThrow(
415 'Expected ref to be a function, a string, an object returned by React.createRef(), or null.',
416 );
417 + });
418
418 - await act(() => {
419 - root.render(<div ref={undefined} />);
420 - });
421 -
422 - await act(() => {
423 - root.render({
424 - $$typeof: Symbol.for('react.element'),
425 - type: 'div',
426 - props: {},
427 - key: null,
428 - ref: null,
429 - });
430 - });
431 -
432 - // But this doesn't
419 + // @gate !enableRefAsProp
420 + it('undefined ref on manually inlined React element triggers error', async () => {
421 + const container = document.createElement('div');
422 + const root = ReactDOMClient.createRoot(container);
423 await expect(async () => {
424 await act(() => {
425 root.render({
426 $$typeof: Symbol.for('react.element'),
427 type: 'div',
438 - props: {},
428 + props: {
429 + ref: undefined,
430 + },
431 key: null,
440 - ref: undefined,
432 });
433 });
434 }).rejects.toThrow(
packages/react-noop-renderer/src/createReactNoop.js
+32 -27
@@ -32,6 +32,7 @@ import {
32 ConcurrentRoot,
33 LegacyRoot,
34 } from 'react-reconciler/constants';
35 +import {enableRefAsProp} from 'shared/ReactFeatureFlags';
36
37 type Container = {
38 rootID: string,
@@ -781,6 +782,34 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
782
783 let currentEventPriority = DefaultEventPriority;
784
785 + function createJSXElementForTestComparison(type, props) {
786 + if (__DEV__ && enableRefAsProp) {
787 + const element = {
788 + type: type,
789 + $$typeof: REACT_ELEMENT_TYPE,
790 + key: null,
791 + props: props,
792 + _owner: null,
793 + _store: __DEV__ ? {} : undefined,
794 + };
795 + Object.defineProperty(element, 'ref', {
796 + enumerable: false,
797 + value: null,
798 + });
799 + return element;
800 + } else {
801 + return {
802 + $$typeof: REACT_ELEMENT_TYPE,
803 + type: type,
804 + key: null,
805 + ref: null,
806 + props: props,
807 + _owner: null,
808 + _store: __DEV__ ? {} : undefined,
809 + };
810 + }
811 + }
812 +
813 function childToJSX(child, text) {
814 if (text !== null) {
815 return text;
@@ -818,15 +847,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
847 if (children !== null) {
848 props.children = children;
849 }
821 - return {
822 - $$typeof: REACT_ELEMENT_TYPE,
823 - type: instance.type,
824 - key: null,
825 - ref: null,
826 - props: props,
827 - _owner: null,
828 - _store: __DEV__ ? {} : undefined,
829 - };
850 + return createJSXElementForTestComparison(instance.type, props);
851 }
852 // This is a text instance
853 const textInstance: TextInstance = (child: any);
@@ -858,15 +879,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
879 return null;
880 }
881 if (isArray(children)) {
861 - return {
862 - $$typeof: REACT_ELEMENT_TYPE,
863 - type: REACT_FRAGMENT_TYPE,
864 - key: null,
865 - ref: null,
866 - props: {children},
867 - _owner: null,
868 - _store: __DEV__ ? {} : undefined,
869 - };
882 + return createJSXElementForTestComparison(REACT_FRAGMENT_TYPE, {children});
883 }
884 return children;
885 }
@@ -877,15 +890,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
890 return null;
891 }
892 if (isArray(children)) {
880 - return {
881 - $$typeof: REACT_ELEMENT_TYPE,
882 - type: REACT_FRAGMENT_TYPE,
883 - key: null,
884 - ref: null,
885 - props: {children},
886 - _owner: null,
887 - _store: __DEV__ ? {} : undefined,
888 - };
893 + return createJSXElementForTestComparison(REACT_FRAGMENT_TYPE, {children});
894 }
895 return children;
896 }
packages/react-reconciler/src/ReactChildFiber.js
+14 -1
@@ -42,6 +42,7 @@ import {
42 } from './ReactWorkTags';
43 import isArray from 'shared/isArray';
44 import {checkPropStringCoercion} from 'shared/CheckStringCoercion';
45 +import {enableRefAsProp} from 'shared/ReactFeatureFlags';
46
47 import {
48 createWorkInProgress,
@@ -153,7 +154,19 @@ function coerceRef(
154 current: Fiber | null,
155 element: ReactElement,
156 ) {
156 - const mixedRef = element.ref;
157 + let mixedRef;
158 + if (enableRefAsProp) {
159 + // TODO: This is a temporary, intermediate step. When enableRefAsProp is on,
160 + // we should resolve the `ref` prop during the begin phase of the component
161 + // it's attached to (HostComponent, ClassComponent, etc).
162 +
163 + const refProp = element.props.ref;
164 + mixedRef = refProp !== undefined ? refProp : null;
165 + } else {
166 + // Old behavior.
167 + mixedRef = element.ref;
168 + }
169 +
170 if (
171 mixedRef !== null &&
172 typeof mixedRef !== 'function' &&
packages/react-reconciler/src/ReactFiberBeginWork.js
+22 -3
@@ -110,6 +110,7 @@ import {
110 enableAsyncActions,
111 enablePostpone,
112 enableRenderableContext,
113 + enableRefAsProp,
114 } from 'shared/ReactFeatureFlags';
115 import isArray from 'shared/isArray';
116 import shallowEqual from 'shared/shallowEqual';
@@ -403,6 +404,24 @@ function updateForwardRef(
404 const render = Component.render;
405 const ref = workInProgress.ref;
406
407 + let propsWithoutRef;
408 + if (enableRefAsProp && 'ref' in nextProps) {
409 + // `ref` is just a prop now, but `forwardRef` expects it to not appear in
410 + // the props object. This used to happen in the JSX runtime, but now we do
411 + // it here.
412 + propsWithoutRef = ({}: {[string]: any});
413 + for (const key in nextProps) {
414 + // Since `ref` should only appear in props via the JSX transform, we can
415 + // assume that this is a plain object. So we don't need a
416 + // hasOwnProperty check.
417 + if (key !== 'ref') {
418 + propsWithoutRef[key] = nextProps[key];
419 + }
420 + }
421 + } else {
422 + propsWithoutRef = nextProps;
423 + }
424 +
425 // The rest is a fork of updateFunctionComponent
426 let nextChildren;
427 let hasId;
@@ -417,7 +436,7 @@ function updateForwardRef(
436 current,
437 workInProgress,
438 render,
420 - nextProps,
439 + propsWithoutRef,
440 ref,
441 renderLanes,
442 );
@@ -428,7 +447,7 @@ function updateForwardRef(
447 current,
448 workInProgress,
449 render,
431 - nextProps,
450 + propsWithoutRef,
451 ref,
452 renderLanes,
453 );
@@ -1980,7 +1999,7 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {
1999 );
2000 }
2001 }
1983 - if (workInProgress.ref !== null) {
2002 + if (!enableRefAsProp && workInProgress.ref !== null) {
2003 let info = '';
2004 const componentName = getComponentNameFromType(Component) || 'Unknown';
2005 const ownerName = getCurrentFiberOwnerNameInDevOrNull();
packages/react-reconciler/src/__tests__/ReactFiberRefs-test.js
+5 -1
@@ -27,7 +27,11 @@ describe('ReactFiberRefs', () => {
27
28 test('ref is attached even if there are no other updates (class)', async () => {
29 let component;
30 - class Component extends React.PureComponent {
30 + class Component extends React.Component {
31 + shouldComponentUpdate() {
32 + // This component's output doesn't depend on any props or state
33 + return false;
34 + }
35 render() {
36 Scheduler.log('Render');
37 component = this;
packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.js
+16 -10
@@ -1296,16 +1296,22 @@ describe('ReactIncrementalSideEffects', () => {
1296 }
1297
1298 ReactNoop.render(<Foo show={true} />);
1299 - await expect(async () => await waitForAll([])).toErrorDev(
1300 - 'Warning: Function components cannot be given refs. ' +
1301 - 'Attempts to access this ref will fail. ' +
1302 - 'Did you mean to use React.forwardRef()?\n\n' +
1303 - 'Check the render method ' +
1304 - 'of `Foo`.\n' +
1305 - ' in FunctionComponent (at **)\n' +
1306 - ' in div (at **)\n' +
1307 - ' in Foo (at **)',
1308 - );
1299 +
1300 + if (gate(flags => flags.enableRefAsProp)) {
1301 + await waitForAll([]);
1302 + } else {
1303 + await expect(async () => await waitForAll([])).toErrorDev(
1304 + 'Warning: Function components cannot be given refs. ' +
1305 + 'Attempts to access this ref will fail. ' +
1306 + 'Did you mean to use React.forwardRef()?\n\n' +
1307 + 'Check the render method ' +
1308 + 'of `Foo`.\n' +
1309 + ' in FunctionComponent (at **)\n' +
1310 + ' in div (at **)\n' +
1311 + ' in Foo (at **)',
1312 + );
1313 + }
1314 +
1315 expect(ops).toEqual([
1316 classInstance,
1317 // no call for function components
packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+1
@@ -1174,6 +1174,7 @@ describe('ReactLazy', () => {
1174 expect(root).toMatchRenderedOutput('2');
1175 });
1176
1177 + // @gate !enableRefAsProp || !__DEV__
1178 it('warns about ref on functions for lazy-loaded components', async () => {
1179 const Foo = props => <div />;
1180 const LazyFoo = lazy(() => {
packages/react-reconciler/src/__tests__/ReactMemo-test.js
+2
@@ -44,6 +44,7 @@ describe('memo', () => {
44 return {default: result};
45 }
46
47 + // @gate !enableRefAsProp || !__DEV__
48 it('warns when giving a ref (simple)', async () => {
49 // This test lives outside sharedTests because the wrappers don't forward
50 // refs properly, and they end up affecting the current owner which is used
@@ -62,6 +63,7 @@ describe('memo', () => {
63 ]);
64 });
65
66 + // @gate !enableRefAsProp || !__DEV__
67 it('warns when giving a ref (complex)', async () => {
68 // defaultProps means this won't use SimpleMemoComponent (as of this writing)
69 // SimpleMemoComponent is unobservable tho, so we can't check :)
packages/react-refresh/src/__tests__/ReactFresh-test.js
+7 -2
@@ -3919,12 +3919,17 @@ describe('ReactFresh', () => {
3919 ReactFreshRuntime = require('react-refresh/runtime');
3920 ReactFreshRuntime.injectIntoGlobalHook(global);
3921
3922 + // NOTE: Intentionally using createElement in this test instead of JSX
3923 + // because old versions of React are incompatible with the JSX transform
3924 + // used by our test suite.
3925 const Hello = () => {
3923 - return <div>Hi!</div>;
3926 + const [state] = React.useState('Hi!');
3927 + // Intentionally
3928 + return React.createElement('div', null, state);
3929 };
3930 $RefreshReg$(Hello, 'Hello');
3931 const Component = Hello;
3927 - ReactDOM.render(<Component />, container);
3932 + ReactDOM.render(React.createElement(Component), container);
3933
3934 expect(onCommitFiberRoot).toHaveBeenCalled();
3935 }
packages/react-server/src/ReactFizzServer.js
+34 -3
@@ -32,7 +32,6 @@ import type {ContextSnapshot} from './ReactFizzNewContext';
32 import type {ComponentStackNode} from './ReactFizzComponentStack';
33 import type {TreeContext} from './ReactFizzTreeContext';
34 import type {ThenableState} from './ReactFizzThenable';
35 -import {enableRenderableContext} from 'shared/ReactFeatureFlags';
35 import {describeObjectForErrorMessage} from 'shared/ReactSerializationErrors';
36
37 import {
@@ -145,6 +144,8 @@ import {
144 enableFloat,
145 enableCache,
146 enablePostpone,
147 + enableRenderableContext,
148 + enableRefAsProp,
149 } from 'shared/ReactFeatureFlags';
150
151 import assign from 'shared/assign';
@@ -1663,12 +1664,31 @@ function renderForwardRef(
1664 ): void {
1665 const previousComponentStack = task.componentStack;
1666 task.componentStack = createFunctionComponentStack(task, type.render);
1667 +
1668 + let propsWithoutRef;
1669 + if (enableRefAsProp && 'ref' in props) {
1670 + // `ref` is just a prop now, but `forwardRef` expects it to not appear in
1671 + // the props object. This used to happen in the JSX runtime, but now we do
1672 + // it here.
1673 + propsWithoutRef = ({}: {[string]: any});
1674 + for (const key in props) {
1675 + // Since `ref` should only appear in props via the JSX transform, we can
1676 + // assume that this is a plain object. So we don't need a
1677 + // hasOwnProperty check.
1678 + if (key !== 'ref') {
1679 + propsWithoutRef[key] = props[key];
1680 + }
1681 + }
1682 + } else {
1683 + propsWithoutRef = props;
1684 + }
1685 +
1686 const children = renderWithHooks(
1687 request,
1688 task,
1689 keyPath,
1690 type.render,
1671 - props,
1691 + propsWithoutRef,
1692 ref,
1693 );
1694 const hasId = checkDidRenderIdHook();
@@ -2189,7 +2209,18 @@ function renderNodeDestructive(
2209 const type = element.type;
2210 const key = element.key;
2211 const props = element.props;
2192 - const ref = element.ref;
2212 +
2213 + let ref;
2214 + if (enableRefAsProp) {
2215 + // TODO: This is a temporary, intermediate step. Once the feature
2216 + // flag is removed, we should get the ref off the props object right
2217 + // before using it.
2218 + const refProp = props.ref;
2219 + ref = refProp !== undefined ? refProp : null;
2220 + } else {
2221 + ref = element.ref;
2222 + }
2223 +
2224 const name = getComponentNameFromType(type);
2225 const keyOrIndex =
2226 key == null ? (childIndex === -1 ? 0 : childIndex) : key;
packages/react-server/src/ReactFlightServer.js
+17 -2
@@ -16,6 +16,7 @@ import {
16 enablePostpone,
17 enableTaint,
18 enableServerComponentKeys,
19 + enableRefAsProp,
20 } from 'shared/ReactFeatureFlags';
21
22 import {
@@ -698,6 +699,8 @@ function renderElement(
699 // When the ref moves to the regular props object this will implicitly
700 // throw for functions. We could probably relax it to a DEV warning for other
701 // cases.
702 + // TODO: `ref` is now just a prop when `enableRefAsProp` is on. Should we
703 + // do what the above comment says?
704 throw new Error(
705 'Refs cannot be used in Server Components, nor passed to Client Components.',
706 );
@@ -1267,6 +1270,18 @@ function renderModelDestructive(
1270 }
1271 }
1272
1273 + const props = element.props;
1274 + let ref;
1275 + if (enableRefAsProp) {
1276 + // TODO: This is a temporary, intermediate step. Once the feature
1277 + // flag is removed, we should get the ref off the props object right
1278 + // before using it.
1279 + const refProp = props.ref;
1280 + ref = refProp !== undefined ? refProp : null;
1281 + } else {
1282 + ref = element.ref;
1283 + }
1284 +
1285 // Attempt to render the Server Component.
1286 return renderElement(
1287 request,
@@ -1274,8 +1289,8 @@ function renderModelDestructive(
1289 element.type,
1290 // $FlowFixMe[incompatible-call] the key of an element is null | string
1291 element.key,
1277 - element.ref,
1278 - element.props,
1292 + ref,
1293 + props,
1294 );
1295 }
1296 case REACT_LAZY_TYPE: {
packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.internal.js
+12 -2
@@ -268,6 +268,7 @@ describe('ReactTestRenderer', () => {
268 expect(log).toEqual([null]);
269 });
270
271 + // @gate !enableRefAsProp || !__DEV__
272 it('warns correctly for refs on SFCs', () => {
273 function Bar() {
274 return <div>Hello, world</div>;
@@ -981,9 +982,14 @@ describe('ReactTestRenderer', () => {
982 </div>
983 ));
984
985 + let refFn;
986 +
987 class App extends React.Component {
988 render() {
986 - return <InnerRefed ref={r => (this.ref = r)} />;
989 + refFn = inst => {
990 + this.ref = inst;
991 + };
992 + return <InnerRefed ref={refFn} />;
993 }
994 }
995
@@ -1004,7 +1010,11 @@ describe('ReactTestRenderer', () => {
1010 {
1011 instance: null,
1012 nodeType: 'host',
1007 - props: {},
1013 + props: gate(flags => flags.enableRefAsProp)
1014 + ? {
1015 + ref: refFn,
1016 + }
1017 + : {},
1018 rendered: [],
1019 type: 'span',
1020 },
packages/react/src/__tests__/ReactCreateElement-test.js
+54 -15
@@ -37,7 +37,11 @@ describe('ReactCreateElement', () => {
37 const element = React.createElement(ComponentClass);
38 expect(element.type).toBe(ComponentClass);
39 expect(element.key).toBe(null);
40 - expect(element.ref).toBe(null);
40 + if (gate(flags => flags.enableRefAsProp)) {
41 + expect(element.ref).toBe(null);
42 + } else {
43 + expect(element.ref).toBe(null);
44 + }
45 if (__DEV__) {
46 expect(Object.isFrozen(element)).toBe(true);
47 expect(Object.isFrozen(element.props)).toBe(true);
@@ -86,6 +90,7 @@ describe('ReactCreateElement', () => {
90 );
91 });
92
93 + // @gate !enableRefAsProp
94 it('should warn when `ref` is being accessed', async () => {
95 class Child extends React.Component {
96 render() {
@@ -119,7 +124,11 @@ describe('ReactCreateElement', () => {
124 const element = React.createElement('div');
125 expect(element.type).toBe('div');
126 expect(element.key).toBe(null);
122 - expect(element.ref).toBe(null);
127 + if (gate(flags => flags.enableRefAsProp)) {
128 + expect(element.ref).toBe(null);
129 + } else {
130 + expect(element.ref).toBe(null);
131 + }
132 if (__DEV__) {
133 expect(Object.isFrozen(element)).toBe(true);
134 expect(Object.isFrozen(element.props)).toBe(true);
@@ -150,31 +159,49 @@ describe('ReactCreateElement', () => {
159 expect(element.props.foo).toBe(1);
160 });
161
153 - it('extracts key and ref from the config', () => {
162 + it('extracts key from the rest of the props', () => {
163 const element = React.createElement(ComponentClass, {
164 key: '12',
156 - ref: '34',
165 foo: '56',
166 });
167 expect(element.type).toBe(ComponentClass);
168 expect(element.key).toBe('12');
161 - expect(element.ref).toBe('34');
162 - if (__DEV__) {
163 - expect(Object.isFrozen(element)).toBe(true);
164 - expect(Object.isFrozen(element.props)).toBe(true);
169 + const expectation = {foo: '56'};
170 + Object.freeze(expectation);
171 + expect(element.props).toEqual(expectation);
172 + });
173 +
174 + it('does not extract ref from the rest of the props', () => {
175 + const ref = React.createRef();
176 + const element = React.createElement(ComponentClass, {
177 + key: '12',
178 + ref: ref,
179 + foo: '56',
180 + });
181 + expect(element.type).toBe(ComponentClass);
182 + if (gate(flags => flags.enableRefAsProp)) {
183 + expect(() => expect(element.ref).toBe(ref)).toErrorDev(
184 + 'Accessing element.ref is no longer supported',
185 + {withoutStack: true},
186 + );
187 + const expectation = {foo: '56', ref};
188 + Object.freeze(expectation);
189 + expect(element.props).toEqual(expectation);
190 + } else {
191 + const expectation = {foo: '56'};
192 + Object.freeze(expectation);
193 + expect(element.props).toEqual(expectation);
194 + expect(element.ref).toBe(ref);
195 }
166 - expect(element.props).toEqual({foo: '56'});
196 });
197
169 - it('extracts null key and ref', () => {
198 + it('extracts null key', () => {
199 const element = React.createElement(ComponentClass, {
200 key: null,
172 - ref: null,
201 foo: '12',
202 });
203 expect(element.type).toBe(ComponentClass);
204 expect(element.key).toBe('null');
177 - expect(element.ref).toBe(null);
205 if (__DEV__) {
206 expect(Object.isFrozen(element)).toBe(true);
207 expect(Object.isFrozen(element.props)).toBe(true);
@@ -191,7 +218,11 @@ describe('ReactCreateElement', () => {
218 const element = React.createElement(ComponentClass, props);
219 expect(element.type).toBe(ComponentClass);
220 expect(element.key).toBe(null);
194 - expect(element.ref).toBe(null);
221 + if (gate(flags => flags.enableRefAsProp)) {
222 + expect(element.ref).toBe(null);
223 + } else {
224 + expect(element.ref).toBe(null);
225 + }
226 if (__DEV__) {
227 expect(Object.isFrozen(element)).toBe(true);
228 expect(Object.isFrozen(element.props)).toBe(true);
@@ -203,7 +234,11 @@ describe('ReactCreateElement', () => {
234 const elementA = React.createElement('div');
235 const elementB = React.createElement('div', elementA.props);
236 expect(elementB.key).toBe(null);
206 - expect(elementB.ref).toBe(null);
237 + if (gate(flags => flags.enableRefAsProp)) {
238 + expect(elementB.ref).toBe(null);
239 + } else {
240 + expect(elementB.ref).toBe(null);
241 + }
242 });
243
244 it('coerces the key to a string', () => {
@@ -213,7 +248,11 @@ describe('ReactCreateElement', () => {
248 });
249 expect(element.type).toBe(ComponentClass);
250 expect(element.key).toBe('12');
216 - expect(element.ref).toBe(null);
251 + if (gate(flags => flags.enableRefAsProp)) {
252 + expect(element.ref).toBe(null);
253 + } else {
254 + expect(element.ref).toBe(null);
255 + }
256 if (__DEV__) {
257 expect(Object.isFrozen(element)).toBe(true);
258 expect(Object.isFrozen(element.props)).toBe(true);
packages/react/src/__tests__/ReactElementClone-test.js
+31 -6
@@ -18,6 +18,8 @@ describe('ReactElementClone', () => {
18 let ComponentClass;
19
20 beforeEach(() => {
21 + jest.resetModules();
22 +
23 act = require('internal-test-utils').act;
24
25 PropTypes = require('prop-types');
@@ -212,7 +214,11 @@ describe('ReactElementClone', () => {
214 ref: this.xyzRef,
215 });
216 expect(clone.key).toBe('xyz');
215 - expect(clone.ref).toBe(this.xyzRef);
217 + if (gate(flags => flags.enableRefAsProp)) {
218 + expect(clone.props.ref).toBe(this.xyzRef);
219 + } else {
220 + expect(clone.ref).toBe(this.xyzRef);
221 + }
222 return <div>{clone}</div>;
223 }
224 }
@@ -368,7 +374,11 @@ describe('ReactElementClone', () => {
374 const elementA = React.createElement('div');
375 const elementB = React.cloneElement(elementA, elementA.props);
376 expect(elementB.key).toBe(null);
371 - expect(elementB.ref).toBe(null);
377 + if (gate(flags => flags.enableRefAsProp)) {
378 + expect(elementB.ref).toBe(null);
379 + } else {
380 + expect(elementB.ref).toBe(null);
381 + }
382 });
383
384 it('should ignore undefined key and ref', () => {
@@ -385,12 +395,21 @@ describe('ReactElementClone', () => {
395 const clone = React.cloneElement(element, props);
396 expect(clone.type).toBe(ComponentClass);
397 expect(clone.key).toBe('12');
388 - expect(clone.ref).toBe('34');
398 + if (gate(flags => flags.enableRefAsProp)) {
399 + expect(clone.props.ref).toBe('34');
400 + expect(() => expect(clone.ref).toBe('34')).toErrorDev(
401 + 'Accessing element.ref is no longer supported',
402 + {withoutStack: true},
403 + );
404 + expect(clone.props).toEqual({foo: 'ef', ref: '34'});
405 + } else {
406 + expect(clone.ref).toBe('34');
407 + expect(clone.props).toEqual({foo: 'ef'});
408 + }
409 if (__DEV__) {
410 expect(Object.isFrozen(element)).toBe(true);
411 expect(Object.isFrozen(element.props)).toBe(true);
412 }
393 - expect(clone.props).toEqual({foo: 'ef'});
413 });
414
415 it('should extract null key and ref', () => {
@@ -407,12 +426,18 @@ describe('ReactElementClone', () => {
426 const clone = React.cloneElement(element, props);
427 expect(clone.type).toBe(ComponentClass);
428 expect(clone.key).toBe('null');
410 - expect(clone.ref).toBe(null);
429 + if (gate(flags => flags.enableRefAsProp)) {
430 + expect(clone.ref).toBe(null);
431 + expect(clone.props).toEqual({foo: 'ef', ref: null});
432 + } else {
433 + expect(clone.ref).toBe(null);
434 + expect(clone.props).toEqual({foo: 'ef'});
435 + }
436 +
437 if (__DEV__) {
438 expect(Object.isFrozen(element)).toBe(true);
439 expect(Object.isFrozen(element.props)).toBe(true);
440 }
415 - expect(clone.props).toEqual({foo: 'ef'});
441 });
442
443 it('throws an error if passed null', () => {
packages/react/src/__tests__/ReactJSXElementValidator-test.js
+9 -3
@@ -389,9 +389,15 @@ describe('ReactJSXElementValidator', () => {
389 }
390 }
391
392 - expect(() => ReactTestUtils.renderIntoDocument(<Foo />)).toErrorDev(
393 - 'Invalid attribute `ref` supplied to `React.Fragment`.',
394 - );
392 + if (gate(flags => flags.enableRefAsProp)) {
393 + expect(() => ReactTestUtils.renderIntoDocument(<Foo />)).toErrorDev(
394 + 'Invalid prop `ref` supplied to `React.Fragment`.',
395 + );
396 + } else {
397 + expect(() => ReactTestUtils.renderIntoDocument(<Foo />)).toErrorDev(
398 + 'Invalid attribute `ref` supplied to `React.Fragment`.',
399 + );
400 + }
401 });
402
403 it('does not warn for fragments of multiple elements without keys', () => {
packages/react/src/__tests__/ReactJSXRuntime-test.js
+1
@@ -220,6 +220,7 @@ describe('ReactJSXRuntime', () => {
220 );
221 });
222
223 + // @gate !enableRefAsProp
224 it('should warn when `ref` is being accessed', async () => {
225 const container = document.createElement('div');
226 class Child extends React.Component {
packages/react/src/__tests__/ReactJSXTransformIntegration-test.js
+42 -8
@@ -55,7 +55,11 @@ describe('ReactJSXTransformIntegration', () => {
55 const element = <Component />;
56 expect(element.type).toBe(Component);
57 expect(element.key).toBe(null);
58 - expect(element.ref).toBe(null);
58 + if (gate(flags => flags.enableRefAsProp)) {
59 + expect(element.ref).toBe(null);
60 + } else {
61 + expect(element.ref).toBe(null);
62 + }
63 const expectation = {};
64 Object.freeze(expectation);
65 expect(element.props).toEqual(expectation);
@@ -65,7 +69,11 @@ describe('ReactJSXTransformIntegration', () => {
69 const element = <div />;
70 expect(element.type).toBe('div');
71 expect(element.key).toBe(null);
68 - expect(element.ref).toBe(null);
72 + if (gate(flags => flags.enableRefAsProp)) {
73 + expect(element.ref).toBe(null);
74 + } else {
75 + expect(element.ref).toBe(null);
76 + }
77 const expectation = {};
78 Object.freeze(expectation);
79 expect(element.props).toEqual(expectation);
@@ -76,7 +84,11 @@ describe('ReactJSXTransformIntegration', () => {
84 const element = <TagName />;
85 expect(element.type).toBe('div');
86 expect(element.key).toBe(null);
79 - expect(element.ref).toBe(null);
87 + if (gate(flags => flags.enableRefAsProp)) {
88 + expect(element.ref).toBe(null);
89 + } else {
90 + expect(element.ref).toBe(null);
91 + }
92 const expectation = {};
93 Object.freeze(expectation);
94 expect(element.props).toEqual(expectation);
@@ -99,22 +111,44 @@ describe('ReactJSXTransformIntegration', () => {
111 expect(element.props.foo).toBe(1);
112 });
113
102 - it('extracts key and ref from the rest of the props', () => {
103 - const ref = React.createRef();
104 - const element = <Component key="12" ref={ref} foo="56" />;
114 + it('extracts key from the rest of the props', () => {
115 + const element = <Component key="12" foo="56" />;
116 expect(element.type).toBe(Component);
117 expect(element.key).toBe('12');
107 - expect(element.ref).toBe(ref);
118 const expectation = {foo: '56'};
119 Object.freeze(expectation);
120 expect(element.props).toEqual(expectation);
121 });
122
123 + it('does not extract ref from the rest of the props', () => {
124 + const ref = React.createRef();
125 + const element = <Component ref={ref} foo="56" />;
126 + expect(element.type).toBe(Component);
127 + if (gate(flags => flags.enableRefAsProp)) {
128 + expect(() => expect(element.ref).toBe(ref)).toErrorDev(
129 + 'Accessing element.ref is no longer supported',
130 + {withoutStack: true},
131 + );
132 + const expectation = {foo: '56', ref};
133 + Object.freeze(expectation);
134 + expect(element.props).toEqual(expectation);
135 + } else {
136 + const expectation = {foo: '56'};
137 + Object.freeze(expectation);
138 + expect(element.props).toEqual(expectation);
139 + expect(element.ref).toBe(ref);
140 + }
141 + });
142 +
143 it('coerces the key to a string', () => {
144 const element = <Component key={12} foo="56" />;
145 expect(element.type).toBe(Component);
146 expect(element.key).toBe('12');
117 - expect(element.ref).toBe(null);
147 + if (gate(flags => flags.enableRefAsProp)) {
148 + expect(element.ref).toBe(null);
149 + } else {
150 + expect(element.ref).toBe(null);
151 + }
152 const expectation = {foo: '56'};
153 Object.freeze(expectation);
154 expect(element.props).toEqual(expectation);
packages/react/src/jsx/ReactJSXElement.js
+154 -56
@@ -21,6 +21,7 @@ import isValidElementType from 'shared/isValidElementType';
21 import isArray from 'shared/isArray';
22 import {describeUnknownElementTypeFrameInDEV} from 'shared/ReactComponentStackFrame';
23 import checkPropTypes from 'shared/checkPropTypes';
24 +import {enableRefAsProp} from 'shared/ReactFeatureFlags';
25
26 const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
27 const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
@@ -30,9 +31,11 @@ const REACT_CLIENT_REFERENCE = Symbol.for('react.client.reference');
31 let specialPropKeyWarningShown;
32 let specialPropRefWarningShown;
33 let didWarnAboutStringRefs;
34 +let didWarnAboutElementRef;
35
36 if (__DEV__) {
37 didWarnAboutStringRefs = {};
38 + didWarnAboutElementRef = {};
39 }
40
41 function hasValidRef(config) {
@@ -111,24 +114,45 @@ function defineKeyPropWarningGetter(props, displayName) {
114 }
115
116 function defineRefPropWarningGetter(props, displayName) {
117 + if (!enableRefAsProp) {
118 + if (__DEV__) {
119 + const warnAboutAccessingRef = function () {
120 + if (!specialPropRefWarningShown) {
121 + specialPropRefWarningShown = true;
122 + console.error(
123 + '%s: `ref` is not a prop. Trying to access it will result ' +
124 + 'in `undefined` being returned. If you need to access the same ' +
125 + 'value within the child component, you should pass it as a different ' +
126 + 'prop. (https://reactjs.org/link/special-props)',
127 + displayName,
128 + );
129 + }
130 + };
131 + warnAboutAccessingRef.isReactWarning = true;
132 + Object.defineProperty(props, 'ref', {
133 + get: warnAboutAccessingRef,
134 + configurable: true,
135 + });
136 + }
137 + }
138 +}
139 +
140 +function elementRefGetterWithDeprecationWarning() {
141 if (__DEV__) {
115 - const warnAboutAccessingRef = function () {
116 - if (!specialPropRefWarningShown) {
117 - specialPropRefWarningShown = true;
118 - console.error(
119 - '%s: `ref` is not a prop. Trying to access it will result ' +
120 - 'in `undefined` being returned. If you need to access the same ' +
121 - 'value within the child component, you should pass it as a different ' +
122 - 'prop. (https://reactjs.org/link/special-props)',
123 - displayName,
124 - );
125 - }
126 - };
127 - warnAboutAccessingRef.isReactWarning = true;
128 - Object.defineProperty(props, 'ref', {
129 - get: warnAboutAccessingRef,
130 - configurable: true,
131 - });
142 + const componentName = getComponentNameFromType(this.type);
143 + if (!didWarnAboutElementRef[componentName]) {
144 + didWarnAboutElementRef[componentName] = true;
145 + console.error(
146 + 'Accessing element.ref is no longer supported. ref is now a ' +
147 + 'regular prop. It will be removed from the JSX Element ' +
148 + 'type in a future release.',
149 + );
150 + }
151 +
152 + // An undefined `element.ref` is coerced to `null` for
153 + // backwards compatibility.
154 + const refProp = this.props.ref;
155 + return refProp !== undefined ? refProp : null;
156 }
157 }
158
@@ -152,20 +176,85 @@ function defineRefPropWarningGetter(props, displayName) {
176 * indicating filename, line number, and/or other information.
177 * @internal
178 */
155 -function ReactElement(type, key, ref, self, source, owner, props) {
156 - const element = {
157 - // This tag allows us to uniquely identify this as a React Element
158 - $$typeof: REACT_ELEMENT_TYPE,
179 +function ReactElement(type, key, _ref, self, source, owner, props) {
180 + let ref;
181 + if (enableRefAsProp) {
182 + // When enableRefAsProp is on, ignore whatever was passed as the ref
183 + // argument and treat `props.ref` as the source of truth. The only thing we
184 + // use this for is `element.ref`, which will log a deprecation warning on
185 + // access. In the next release, we can remove `element.ref` as well as the
186 + // `ref` argument.
187 + const refProp = props.ref;
188 +
189 + // An undefined `element.ref` is coerced to `null` for
190 + // backwards compatibility.
191 + ref = refProp !== undefined ? refProp : null;
192 + } else {
193 + ref = _ref;
194 + }
195
160 - // Built-in properties that belong on the element
161 - type,
162 - key,
163 - ref,
164 - props,
196 + let element;
197 + if (__DEV__ && enableRefAsProp) {
198 + // In dev, make `ref` a non-enumerable property with a warning. It's non-
199 + // enumerable so that test matchers and serializers don't access it and
200 + // trigger the warning.
201 + //
202 + // `ref` will be removed from the element completely in a future release.
203 + element = {
204 + // This tag allows us to uniquely identify this as a React Element
205 + $$typeof: REACT_ELEMENT_TYPE,
206 +
207 + // Built-in properties that belong on the element
208 + type,
209 + key,
210
166 - // Record the component responsible for creating this element.
167 - _owner: owner,
168 - };
211 + props,
212 +
213 + // Record the component responsible for creating this element.
214 + _owner: owner,
215 + };
216 + if (ref !== null) {
217 + Object.defineProperty(element, 'ref', {
218 + enumerable: false,
219 + get: elementRefGetterWithDeprecationWarning,
220 + });
221 + } else {
222 + // Don't warn on access if a ref is not given. This reduces false
223 + // positives in cases where a test serializer uses
224 + // getOwnPropertyDescriptors to compare objects, like Jest does, which is
225 + // a problem because it bypasses non-enumerability.
226 + //
227 + // So unfortunately this will trigger a false positive warning in Jest
228 + // when the diff is printed:
229 + //
230 + // expect(<div ref={ref} />).toEqual(<span ref={ref} />);
231 + //
232 + // A bit sketchy, but this is what we've done for the `props.key` and
233 + // `props.ref` accessors for years, which implies it will be good enough
234 + // for `element.ref`, too. Let's see if anyone complains.
235 + Object.defineProperty(element, 'ref', {
236 + enumerable: false,
237 + value: null,
238 + });
239 + }
240 + } else {
241 + // In prod, `ref` is a regular property. It will be removed in a
242 + // future release.
243 + element = {
244 + // This tag allows us to uniquely identify this as a React Element
245 + $$typeof: REACT_ELEMENT_TYPE,
246 +
247 + // Built-in properties that belong on the element
248 + type,
249 + key,
250 + ref,
251 +
252 + props,
253 +
254 + // Record the component responsible for creating this element.
255 + _owner: owner,
256 + };
257 + }
258
259 if (__DEV__) {
260 // The validation flag is currently mutative. We put it on
@@ -236,7 +325,9 @@ export function jsxProd(type, config, maybeKey) {
325 }
326
327 if (hasValidRef(config)) {
239 - ref = config.ref;
328 + if (!enableRefAsProp) {
329 + ref = config.ref;
330 + }
331 }
332
333 // Remaining properties are added to a new props object
@@ -245,8 +336,7 @@ export function jsxProd(type, config, maybeKey) {
336 hasOwnProperty.call(config, propName) &&
337 // Skip over reserved prop names
338 propName !== 'key' &&
248 - // TODO: `ref` will no longer be reserved in the next major
249 - propName !== 'ref'
339 + (enableRefAsProp || propName !== 'ref')
340 ) {
341 props[propName] = config[propName];
342 }
@@ -453,7 +543,9 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) {
543 }
544
545 if (hasValidRef(config)) {
456 - ref = config.ref;
546 + if (!enableRefAsProp) {
547 + ref = config.ref;
548 + }
549 warnIfStringRefCannotBeAutoConverted(config, self);
550 }
551
@@ -463,8 +555,7 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) {
555 hasOwnProperty.call(config, propName) &&
556 // Skip over reserved prop names
557 propName !== 'key' &&
466 - // TODO: `ref` will no longer be reserved in the next major
467 - propName !== 'ref'
558 + (enableRefAsProp || propName !== 'ref')
559 ) {
560 props[propName] = config[propName];
561 }
@@ -480,7 +571,7 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) {
571 }
572 }
573
483 - if (key || ref) {
574 + if (key || (!enableRefAsProp && ref)) {
575 const displayName =
576 typeof type === 'function'
577 ? type.displayName || type.name || 'Unknown'
@@ -488,7 +579,7 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) {
579 if (key) {
580 defineKeyPropWarningGetter(props, displayName);
581 }
491 - if (ref) {
582 + if (!enableRefAsProp && ref) {
583 defineRefPropWarningGetter(props, displayName);
584 }
585 }
@@ -589,7 +680,9 @@ export function createElement(type, config, children) {
680
681 if (config != null) {
682 if (hasValidRef(config)) {
592 - ref = config.ref;
683 + if (!enableRefAsProp) {
684 + ref = config.ref;
685 + }
686
687 if (__DEV__) {
688 warnIfStringRefCannotBeAutoConverted(config, config.__self);
@@ -608,14 +701,11 @@ export function createElement(type, config, children) {
701 hasOwnProperty.call(config, propName) &&
702 // Skip over reserved prop names
703 propName !== 'key' &&
611 - // TODO: `ref` will no longer be reserved in the next major
612 - propName !== 'ref' &&
613 - // ...and maybe these, too, though we currently rely on them for
614 - // warnings and debug information in dev. Need to decide if we're OK
615 - // with dropping them. In the jsx() runtime it's not an issue because
616 - // the data gets passed as separate arguments instead of props, but
617 - // it would be nice to stop relying on them entirely so we can drop
618 - // them from the internal Fiber field.
704 + (enableRefAsProp || propName !== 'ref') &&
705 + // Even though we don't use these anymore in the runtime, we don't want
706 + // them to appear as props, so in createElement we filter them out.
707 + // We don't have to do this in the jsx() runtime because the jsx()
708 + // transform never passed these as props; it used separate arguments.
709 propName !== '__self' &&
710 propName !== '__source'
711 ) {
@@ -652,7 +742,7 @@ export function createElement(type, config, children) {
742 }
743 }
744 if (__DEV__) {
655 - if (key || ref) {
745 + if (key || (!enableRefAsProp && ref)) {
746 const displayName =
747 typeof type === 'function'
748 ? type.displayName || type.name || 'Unknown'
@@ -660,7 +750,7 @@ export function createElement(type, config, children) {
750 if (key) {
751 defineKeyPropWarningGetter(props, displayName);
752 }
663 - if (ref) {
753 + if (!enableRefAsProp && ref) {
754 defineRefPropWarningGetter(props, displayName);
755 }
756 }
@@ -732,7 +822,9 @@ export function cloneAndReplaceKey(oldElement, newKey) {
822 return ReactElement(
823 oldElement.type,
824 newKey,
735 - oldElement.ref,
825 + // When enableRefAsProp is on, this argument is ignored. This check only
826 + // exists to avoid the `ref` access warning.
827 + enableRefAsProp ? null : oldElement.ref,
828 undefined,
829 undefined,
830 oldElement._owner,
@@ -758,15 +850,17 @@ export function cloneElement(element, config, children) {
850
851 // Reserved names are extracted
852 let key = element.key;
761 - let ref = element.ref;
853 + let ref = enableRefAsProp ? null : element.ref;
854
855 // Owner will be preserved, unless ref is overridden
856 let owner = element._owner;
857
858 if (config != null) {
859 if (hasValidRef(config)) {
768 - // Silently steal the ref from the parent.
769 - ref = config.ref;
860 + if (!enableRefAsProp) {
861 + // Silently steal the ref from the parent.
862 + ref = config.ref;
863 + }
864 owner = ReactCurrentOwner.current;
865 }
866 if (hasValidKey(config)) {
@@ -786,8 +880,7 @@ export function cloneElement(element, config, children) {
880 hasOwnProperty.call(config, propName) &&
881 // Skip over reserved prop names
882 propName !== 'key' &&
789 - // TODO: `ref` will no longer be reserved in the next major
790 - propName !== 'ref' &&
883 + (enableRefAsProp || propName !== 'ref') &&
884 // ...and maybe these, too, though we currently rely on them for
885 // warnings and debug information in dev. Need to decide if we're OK
886 // with dropping them. In the jsx() runtime it's not an issue because
@@ -795,7 +888,11 @@ export function cloneElement(element, config, children) {
888 // it would be nice to stop relying on them entirely so we can drop
889 // them from the internal Fiber field.
890 propName !== '__self' &&
798 - propName !== '__source'
891 + propName !== '__source' &&
892 + // Undefined `ref` is ignored by cloneElement. We treat it the same as
893 + // if the property were missing. This is mostly for
894 + // backwards compatibility.
895 + !(enableRefAsProp && propName === 'ref' && config.ref === undefined)
896 ) {
897 if (config[propName] === undefined && defaultProps !== undefined) {
898 // Resolve default props
@@ -1016,6 +1113,7 @@ function getCurrentComponentErrorInfo(parentType) {
1113 * @param {ReactElement} fragment
1114 */
1115 function validateFragmentProps(fragment) {
1116 + // TODO: Move this to render phase instead of at element creation.
1117 if (__DEV__) {
1118 const keys = Object.keys(fragment.props);
1119 for (let i = 0; i < keys.length; i++) {
@@ -1032,7 +1130,7 @@ function validateFragmentProps(fragment) {
1130 }
1131 }
1132
1035 - if (fragment.ref !== null) {
1133 + if (!enableRefAsProp && fragment.ref !== null) {
1134 setCurrentlyValidatingElement(fragment);
1135 console.error('Invalid attribute `ref` supplied to `React.Fragment`.');
1136 setCurrentlyValidatingElement(null);
packages/shared/ReactFeatureFlags.js
+7
@@ -178,6 +178,13 @@ export const enableServerComponentKeys = __NEXT_MAJOR__;
178 */
179 export const enableInfiniteRenderLoopDetection = true;
180
181 +// Subtle breaking changes to JSX runtime to make it faster, like passing `ref`
182 +// as a normal prop instead of stripping it from the props object.
183 +
184 +// Passes `ref` as a normal prop instead of stripping it from the props object
185 +// during element creation.
186 +export const enableRefAsProp = __NEXT_MAJOR__;
187 +
188 // -----------------------------------------------------------------------------
189 // Chopping Block
190 //
packages/shared/forks/ReactFeatureFlags.native-fb.js
+4
@@ -97,5 +97,9 @@ export const disableClientCache = true;
97 export const enableServerComponentKeys = true;
98 export const enableInfiniteRenderLoopDetection = false;
99
100 +// TODO: Roll out with GK. Don't keep as dynamic flag for too long, though,
101 +// because JSX is an extremely hot path.
102 +export const enableRefAsProp = false;
103 +
104 // Flow magic to verify the exports of this file match the original version.
105 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.native-oss.js
+3
@@ -89,5 +89,8 @@ export const disableClientCache = true;
89
90 export const enableServerComponentKeys = true;
91
92 +// TODO: Should turn this on in next "major" RN release.
93 +export const enableRefAsProp = false;
94 +
95 // Flow magic to verify the exports of this file match the original version.
96 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+9
@@ -89,5 +89,14 @@ export const disableClientCache = true;
89 export const enableServerComponentKeys = true;
90 export const enableInfiniteRenderLoopDetection = false;
91
92 +// TODO: This must be in sync with the main ReactFeatureFlags file because
93 +// the Test Renderer's value must be the same as the one used by the
94 +// react package.
95 +//
96 +// We really need to get rid of this whole module. Any test renderer specific
97 +// flags should be handled by the Fiber config.
98 +const __NEXT_MAJOR__ = __EXPERIMENTAL__;
99 +export const enableRefAsProp = __NEXT_MAJOR__;
100 +
101 // Flow magic to verify the exports of this file match the original version.
102 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+2
@@ -86,5 +86,7 @@ export const disableClientCache = true;
86
87 export const enableServerComponentKeys = true;
88
89 +export const enableRefAsProp = false;
90 +
91 // Flow magic to verify the exports of this file match the original version.
92 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+2
@@ -89,5 +89,7 @@ export const disableClientCache = true;
89 export const enableServerComponentKeys = true;
90 export const enableInfiniteRenderLoopDetection = false;
91
92 +export const enableRefAsProp = false;
93 +
94 // Flow magic to verify the exports of this file match the original version.
95 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.www.js
+4
@@ -116,5 +116,9 @@ export const disableClientCache = true;
116
117 export const enableServerComponentKeys = true;
118
119 +// TODO: Roll out with GK. Don't keep as dynamic flag for too long, though,
120 +// because JSX is an extremely hot path.
121 +export const enableRefAsProp = false;
122 +
123 // Flow magic to verify the exports of this file match the original version.
124 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
scripts/jest/TestFlags.js
+11 -2
@@ -78,12 +78,21 @@ function getTestFlags() {
78 source: !process.env.IS_BUILD,
79 www,
80
81 - // This isn't a flag, just a useful alias for tests.
81 + // These aren't flags, just a useful aliases for tests.
82 enableActivity: releaseChannel === 'experimental' || www,
83 - enableUseSyncExternalStoreShim: !__VARIANT__,
83 enableSuspenseList: releaseChannel === 'experimental' || www,
84 enableLegacyHidden: www,
85
86 + // This is used by useSyncExternalStoresShared-test.js to decide whether
87 + // to test the shim or the native implementation of useSES.
88 + // TODO: It's disabled when enableRefAsProp is on because the JSX
89 + // runtime used by our tests is not compatible with older versions of
90 + // React. If we want to keep testing this shim after enableRefIsProp is
91 + // on everywhere, we'll need to find some other workaround. Maybe by
92 + // only using createElement instead of JSX in that test module.
93 + enableUseSyncExternalStoreShim:
94 + !__VARIANT__ && !featureFlags.enableRefAsProp,
95 +
96 // If there's a naming conflict between scheduler and React feature flags, the
97 // React ones take precedence.
98 // TODO: Maybe we should error on conflicts? Or we could namespace