diff --git a/packages/react-art/src/ReactFiberConfigART.js b/packages/react-art/src/ReactFiberConfigART.js index 1ca506d022f4e..5b8788453f6af 100644 --- a/packages/react-art/src/ReactFiberConfigART.js +++ b/packages/react-art/src/ReactFiberConfigART.js @@ -318,6 +318,27 @@ export function cloneMutableTextInstance(textInstance) { return textInstance; } +export type FragmentInstanceType = null; + +export function createFragmentInstance(fiber): null { + return null; +} + +export function updateFragmentInstanceFiber(fiber, instance): void { + // Noop +} + +export function commitNewChildToFragmentInstance( + child, + fragmentInstance, +): void { + // Noop +} + +export function deleteChildFromFragmentInstance(child, fragmentInstance): void { + // Noop +} + export function finalizeInitialChildren(domElement, type, props) { return false; } diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 47d6a4cb24806..efbaebe2ab06c 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -34,6 +34,7 @@ import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostCo import hasOwnProperty from 'shared/hasOwnProperty'; import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion'; import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; +import {OffscreenComponent} from 'react-reconciler/src/ReactWorkTags'; export { setCurrentUpdatePriority, @@ -2076,6 +2077,235 @@ export function subscribeToGestureDirection( } } +type EventListenerOptionsOrUseCapture = + | boolean + | { + capture?: boolean, + once?: boolean, + passive?: boolean, + signal?: AbortSignal, + ... + }; + +type StoredEventListener = { + type: string, + listener: EventListener, + optionsOrUseCapture: void | EventListenerOptionsOrUseCapture, +}; + +export type FragmentInstanceType = { + _fragmentFiber: Fiber, + _eventListeners: null | Array, + addEventListener( + type: string, + listener: EventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture, + ): void, + removeEventListener( + type: string, + listener: EventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture, + ): void, + focus(): void, +}; + +function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) { + this._fragmentFiber = fragmentFiber; + this._eventListeners = null; +} +// $FlowFixMe[prop-missing] +FragmentInstance.prototype.addEventListener = function ( + this: FragmentInstanceType, + type: string, + listener: EventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture, +): void { + if (this._eventListeners === null) { + this._eventListeners = []; + } + + const listeners = this._eventListeners; + // Element.addEventListener will only apply uniquely new event listeners by default. Since we + // need to collect the listeners to apply to appended children, we track them ourselves and use + // custom equality check for the options. + const isNewEventListener = + indexOfEventListener(listeners, type, listener, optionsOrUseCapture) === -1; + if (isNewEventListener) { + listeners.push({type, listener, optionsOrUseCapture}); + traverseFragmentInstanceChildren( + this, + this._fragmentFiber.child, + addEventListenerToChild, + type, + listener, + optionsOrUseCapture, + ); + } + this._eventListeners = listeners; +}; +function addEventListenerToChild( + child: Instance, + type: string, + listener: EventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture, +): boolean { + child.addEventListener(type, listener, optionsOrUseCapture); + return false; +} +// $FlowFixMe[prop-missing] +FragmentInstance.prototype.removeEventListener = function ( + this: FragmentInstanceType, + type: string, + listener: EventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture, +): void { + const listeners = this._eventListeners; + if (listeners === null) { + return; + } + if (typeof listeners !== 'undefined' && listeners.length > 0) { + traverseFragmentInstanceChildren( + this, + this._fragmentFiber.child, + removeEventListenerFromChild, + type, + listener, + optionsOrUseCapture, + ); + const index = indexOfEventListener( + listeners, + type, + listener, + optionsOrUseCapture, + ); + if (this._eventListeners !== null) { + this._eventListeners.splice(index, 1); + } + } +}; +function removeEventListenerFromChild( + child: Instance, + type: string, + listener: EventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture, +): boolean { + child.removeEventListener(type, listener, optionsOrUseCapture); + return false; +} +// $FlowFixMe[prop-missing] +FragmentInstance.prototype.focus = function (this: FragmentInstanceType) { + traverseFragmentInstanceChildren( + this, + this._fragmentFiber.child, + setFocusIfFocusable, + ); +}; + +function traverseFragmentInstanceChildren( + fragmentInstance: FragmentInstanceType, + child: Fiber | null, + fn: (Instance, A, B, C) => boolean, + a: A, + b: B, + c: C, +): void { + while (child !== null) { + if (child.tag === HostComponent) { + if (fn(child.stateNode, a, b, c)) { + return; + } + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null + ) { + // Skip hidden subtrees + } else { + traverseFragmentInstanceChildren( + fragmentInstance, + child.child, + fn, + a, + b, + c, + ); + } + child = child.sibling; + } +} + +function normalizeListenerOptions( + opts: ?EventListenerOptionsOrUseCapture, +): string { + if (opts == null) { + return '0'; + } + + if (typeof opts === 'boolean') { + return `c=${opts ? '1' : '0'}`; + } + + return `c=${opts.capture ? '1' : '0'}&o=${opts.once ? '1' : '0'}&p=${opts.passive ? '1' : '0'}`; +} + +function indexOfEventListener( + eventListeners: Array, + type: string, + listener: EventListener, + optionsOrUseCapture: void | EventListenerOptionsOrUseCapture, +): number { + for (let i = 0; i < eventListeners.length; i++) { + const item = eventListeners[i]; + if ( + item.type === type && + item.listener === listener && + normalizeListenerOptions(item.optionsOrUseCapture) === + normalizeListenerOptions(optionsOrUseCapture) + ) { + return i; + } + } + return -1; +} + +export function createFragmentInstance( + fragmentFiber: Fiber, +): FragmentInstanceType { + return new (FragmentInstance: any)(fragmentFiber); +} + +export function updateFragmentInstanceFiber( + fragmentFiber: Fiber, + instance: FragmentInstanceType, +): void { + instance._fragmentFiber = fragmentFiber; +} + +export function commitNewChildToFragmentInstance( + childElement: Instance, + fragmentInstance: FragmentInstanceType, +): void { + const eventListeners = fragmentInstance._eventListeners; + if (eventListeners !== null) { + for (let i = 0; i < eventListeners.length; i++) { + const {type, listener, optionsOrUseCapture} = eventListeners[i]; + childElement.addEventListener(type, listener, optionsOrUseCapture); + } + } +} + +export function deleteChildFromFragmentInstance( + childElement: Instance, + fragmentInstance: FragmentInstanceType, +): void { + const eventListeners = fragmentInstance._eventListeners; + if (eventListeners !== null) { + for (let i = 0; i < eventListeners.length; i++) { + const {type, listener, optionsOrUseCapture} = eventListeners[i]; + childElement.removeEventListener(type, listener, optionsOrUseCapture); + } + } +} + export function clearContainer(container: Container): void { const nodeType = container.nodeType; if (nodeType === DOCUMENT_NODE) { diff --git a/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js b/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js new file mode 100644 index 0000000000000..727fd3014201f --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js @@ -0,0 +1,620 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails reactcore + */ + +'use strict'; + +let React; +let ReactDOMClient; +let act; +let container; +let Fragment; +let Activity; + +describe('FragmentRefs', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + Fragment = React.Fragment; + Activity = React.unstable_Activity; + ReactDOMClient = require('react-dom/client'); + act = require('internal-test-utils').act; + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.removeChild(container); + }); + + // @gate enableFragmentRefs + it('attaches a ref to Fragment', async () => { + const fragmentRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + + await act(() => + root.render( +
+ +
Hi
+
+
, + ), + ); + expect(container.innerHTML).toEqual( + '
Hi
', + ); + + expect(fragmentRef.current).not.toBe(null); + }); + + // @gate enableFragmentRefs + it('accepts a ref callback', async () => { + let fragmentRef; + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render( + (fragmentRef = ref)}> +
Hi
+
, + ); + }); + + expect(fragmentRef._fragmentFiber).toBeTruthy(); + }); + + // @gate enableFragmentRefs + it('is available in effects', async () => { + function Test() { + const fragmentRef = React.useRef(null); + React.useLayoutEffect(() => { + expect(fragmentRef.current).not.toBe(null); + }); + React.useEffect(() => { + expect(fragmentRef.current).not.toBe(null); + }); + return ( + +
+ + ); + } + + const root = ReactDOMClient.createRoot(container); + await act(() => root.render()); + }); + + describe('focus()', () => { + // @gate enableFragmentRefs + it('focuses the first focusable child', async () => { + const fragmentRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + + function Test() { + return ( +
+ +
+ + + B + + + C + + +
+ ); + } + + await act(() => { + root.render(); + }); + + await act(() => { + fragmentRef.current.focus(); + }); + expect(document.activeElement.id).toEqual('child-b'); + document.activeElement.blur(); + }); + + // @gate enableFragmentRefs + it('preserves document order when adding and removing children', async () => { + const fragmentRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + + function Test({showA, showB}) { + return ( + + {showA && } + {showB && } + + ); + } + + // Render with A as the first focusable child + await act(() => { + root.render(); + }); + await act(() => { + fragmentRef.current.focus(); + }); + expect(document.activeElement.id).toEqual('child-a'); + document.activeElement.blur(); + // A is still the first focusable child, but B is also tracked + await act(() => { + root.render(); + }); + await act(() => { + fragmentRef.current.focus(); + }); + expect(document.activeElement.id).toEqual('child-a'); + document.activeElement.blur(); + + // B is now the first focusable child + await act(() => { + root.render(); + }); + await act(() => { + fragmentRef.current.focus(); + }); + expect(document.activeElement.id).toEqual('child-b'); + document.activeElement.blur(); + }); + }); + + describe('event listeners', () => { + // @gate enableFragmentRefs + it('adds and removes event listeners from children', async () => { + const parentRef = React.createRef(); + const fragmentRef = React.createRef(); + const childARef = React.createRef(); + const childBRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + + let logs = []; + + function handleFragmentRefClicks() { + logs.push('fragmentRef'); + } + + function Test() { + React.useEffect(() => { + fragmentRef.current.addEventListener( + 'click', + handleFragmentRefClicks, + ); + + return () => { + fragmentRef.current.removeEventListener( + 'click', + handleFragmentRefClicks, + ); + }; + }, []); + return ( +
+ + <>Text +
A
+ <> +
B
+ +
+
+ ); + } + + await act(() => { + root.render(); + }); + + childARef.current.addEventListener('click', () => { + logs.push('A'); + }); + + childBRef.current.addEventListener('click', () => { + logs.push('B'); + }); + + // Clicking on the parent should not trigger any listeners + parentRef.current.click(); + expect(logs).toEqual([]); + + // Clicking a child triggers its own listeners and the Fragment's + childARef.current.click(); + expect(logs).toEqual(['fragmentRef', 'A']); + + logs = []; + + childBRef.current.click(); + expect(logs).toEqual(['fragmentRef', 'B']); + + logs = []; + + fragmentRef.current.removeEventListener('click', handleFragmentRefClicks); + + childARef.current.click(); + expect(logs).toEqual(['A']); + + logs = []; + + childBRef.current.click(); + expect(logs).toEqual(['B']); + }); + + // @gate enableFragmentRefs + it('adds and removes event listeners from children with multiple fragments', async () => { + const fragmentRef = React.createRef(); + const nestedFragmentRef = React.createRef(); + const nestedFragmentRef2 = React.createRef(); + const childARef = React.createRef(); + const childBRef = React.createRef(); + const childCRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render( +
+ +
A
+
+ +
B
+
+
+ +
C
+
+
+
, + ); + }); + + let logs = []; + + function handleFragmentRefClicks() { + logs.push('fragmentRef'); + } + + function handleNestedFragmentRefClicks() { + logs.push('nestedFragmentRef'); + } + + function handleNestedFragmentRef2Clicks() { + logs.push('nestedFragmentRef2'); + } + + fragmentRef.current.addEventListener('click', handleFragmentRefClicks); + nestedFragmentRef.current.addEventListener( + 'click', + handleNestedFragmentRefClicks, + ); + nestedFragmentRef2.current.addEventListener( + 'click', + handleNestedFragmentRef2Clicks, + ); + + childBRef.current.click(); + // Event bubbles to the parent fragment + expect(logs).toEqual(['nestedFragmentRef', 'fragmentRef']); + + logs = []; + + childARef.current.click(); + expect(logs).toEqual(['fragmentRef']); + + logs = []; + childCRef.current.click(); + expect(logs).toEqual(['fragmentRef', 'nestedFragmentRef2']); + + logs = []; + + fragmentRef.current.removeEventListener('click', handleFragmentRefClicks); + nestedFragmentRef.current.removeEventListener( + 'click', + handleNestedFragmentRefClicks, + ); + childCRef.current.click(); + expect(logs).toEqual(['nestedFragmentRef2']); + }); + + // @gate enableFragmentRefs + it('adds an event listener to a newly added child', async () => { + const fragmentRef = React.createRef(); + const childRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + let showChild; + + function Component() { + const [shouldShowChild, setShouldShowChild] = React.useState(false); + showChild = () => { + setShouldShowChild(true); + }; + + return ( +
+ +
A
+ {shouldShowChild && ( +
+ B +
+ )} +
+
+ ); + } + + await act(() => { + root.render(); + }); + + expect(fragmentRef.current).not.toBe(null); + expect(childRef.current).toBe(null); + + let hasClicked = false; + fragmentRef.current.addEventListener('click', () => { + hasClicked = true; + }); + + await act(() => { + showChild(); + }); + expect(childRef.current).not.toBe(null); + + childRef.current.click(); + expect(hasClicked).toBe(true); + }); + + // @gate enableFragmentRefs + it('applies event listeners to host children nested within non-host children', async () => { + const fragmentRef = React.createRef(); + const childRef = React.createRef(); + const nestedChildRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + + function Wrapper({children}) { + return children; + } + + await act(() => { + root.render( +
+ +
Host A
+ + + +
Host B
+
+
+
+
+
, + ); + }); + const logs = []; + fragmentRef.current.addEventListener('click', e => { + logs.push(e.target.textContent); + }); + + expect(logs).toEqual([]); + childRef.current.click(); + expect(logs).toEqual(['Host A']); + nestedChildRef.current.click(); + expect(logs).toEqual(['Host A', 'Host B']); + }); + + // @gate enableFragmentRefs + it('allows adding and cleaning up listeners in effects', async () => { + const root = ReactDOMClient.createRoot(container); + + let logs = []; + function logClick(e) { + logs.push(e.currentTarget.id); + } + + let rerender; + let removeEventListeners; + + function Test() { + const fragmentRef = React.useRef(null); + // eslint-disable-next-line no-unused-vars + const [_, setState] = React.useState(0); + rerender = () => { + setState(p => p + 1); + }; + removeEventListeners = () => { + fragmentRef.current.removeEventListener('click', logClick); + }; + React.useEffect(() => { + fragmentRef.current.addEventListener('click', logClick); + + return removeEventListeners; + }); + + return ( + +
+ + ); + } + + // The event listener was applied + await act(() => root.render()); + expect(logs).toEqual([]); + document.querySelector('#child-a').click(); + expect(logs).toEqual(['child-a']); + + // The event listener can be removed and re-added + logs = []; + await act(rerender); + document.querySelector('#child-a').click(); + expect(logs).toEqual(['child-a']); + }); + + // @gate enableFragmentRefs + it('does not apply removed event listeners to new children', async () => { + const root = ReactDOMClient.createRoot(container); + const fragmentRef = React.createRef(null); + function Test() { + return ( + +
+ + ); + } + + let logs = []; + function logClick(e) { + logs.push(e.currentTarget.id); + } + await act(() => { + root.render(); + }); + fragmentRef.current.addEventListener('click', logClick); + const childA = document.querySelector('#child-a'); + childA.click(); + expect(logs).toEqual(['child-a']); + + logs = []; + fragmentRef.current.removeEventListener('click', logClick); + childA.click(); + expect(logs).toEqual([]); + }); + + describe('with activity', () => { + // @gate enableFragmentRefs && enableActivity + it('does not apply event listeners to hidden trees', async () => { + const parentRef = React.createRef(); + const fragmentRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + + function Test() { + return ( +
+ +
Child 1
+ +
Child 2
+
+
Child 3
+
+
+ ); + } + + await act(() => { + root.render(); + }); + + const logs = []; + fragmentRef.current.addEventListener('click', e => { + logs.push(e.target.textContent); + }); + + const [child1, child2, child3] = parentRef.current.children; + child1.click(); + child2.click(); + child3.click(); + expect(logs).toEqual(['Child 1', 'Child 3']); + }); + + // @gate enableFragmentRefs && enableActivity + it('applies event listeners to visible trees', async () => { + const parentRef = React.createRef(); + const fragmentRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + + function Test() { + return ( +
+ +
Child 1
+ +
Child 2
+
+
Child 3
+
+
+ ); + } + + await act(() => { + root.render(); + }); + + const logs = []; + fragmentRef.current.addEventListener('click', e => { + logs.push(e.target.textContent); + }); + + const [child1, child2, child3] = parentRef.current.children; + child1.click(); + child2.click(); + child3.click(); + expect(logs).toEqual(['Child 1', 'Child 2', 'Child 3']); + }); + + // @gate enableFragmentRefs && enableActivity + it('handles Activity modes switching', async () => { + const fragmentRef = React.createRef(); + const fragmentRef2 = React.createRef(); + const parentRef = React.createRef(); + const root = ReactDOMClient.createRoot(container); + + function Test({mode}) { + return ( +
+ + +
Child
+ +
Child 2
+
+
+
+
+ ); + } + + await act(() => { + root.render(); + }); + + let logs = []; + fragmentRef.current.addEventListener('click', () => { + logs.push('clicked 1'); + }); + fragmentRef2.current.addEventListener('click', () => { + logs.push('clicked 2'); + }); + parentRef.current.lastChild.click(); + expect(logs).toEqual(['clicked 1', 'clicked 2']); + + logs = []; + await act(() => { + root.render(); + }); + parentRef.current.firstChild.click(); + parentRef.current.lastChild.click(); + expect(logs).toEqual([]); + + logs = []; + await act(() => { + root.render(); + }); + parentRef.current.lastChild.click(); + // Event order is flipped here because the nested child re-registers first + expect(logs).toEqual(['clicked 2', 'clicked 1']); + }); + }); + }); +}); diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index 6db223e4b4dc5..c3b8ac4d35d14 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -591,6 +591,35 @@ export function waitForCommitToBeReady(): null { return null; } +export type FragmentInstanceType = null; + +export function createFragmentInstance( + fragmentFiber: Fiber, +): FragmentInstanceType { + return null; +} + +export function updateFragmentInstanceFiber( + fragmentFiber: Fiber, + instance: FragmentInstanceType, +): void { + // Noop +} + +export function commitNewChildToFragmentInstance( + child: PublicInstance, + fragmentInstance: FragmentInstanceType, +): void { + // Noop +} + +export function deleteChildFromFragmentInstance( + child: PublicInstance, + fragmentInstance: FragmentInstanceType, +): void { + // Noop +} + export const NotPendingTransition: TransitionStatus = null; export const HostTransitionContext: ReactContext = { $$typeof: REACT_CONTEXT_TYPE, diff --git a/packages/react-native-renderer/src/ReactFiberConfigNative.js b/packages/react-native-renderer/src/ReactFiberConfigNative.js index 5f1b5583a18d9..b5f07a77b24c2 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigNative.js +++ b/packages/react-native-renderer/src/ReactFiberConfigNative.js @@ -202,6 +202,35 @@ export function cloneMutableTextInstance( throw new Error('Not yet implemented.'); } +export type FragmentInstanceType = null; + +export function createFragmentInstance( + fragmentFiber: Fiber, +): FragmentInstanceType { + return null; +} + +export function updateFragmentInstanceFiber( + fragmentFiber: Fiber, + instance: FragmentInstanceType, +): void { + // Noop +} + +export function commitNewChildToFragmentInstance( + child: PublicInstance, + fragmentInstance: FragmentInstanceType, +): void { + // Noop +} + +export function deleteChildFromFragmentInstance( + child: PublicInstance, + fragmentInstance: FragmentInstanceType, +): void { + // Noop +} + export function finalizeInitialChildren( parentInstance: Instance, type: string, diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 4978dccf2a03c..b41f22b373b54 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -512,6 +512,18 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { throw new Error('Not yet implemented.'); }, + createFragmentInstance(parentInstance) { + return null; + }, + + commitNewChildToFragmentInstance(child, fragmentInstance) { + // Noop + }, + + deleteChildFromFragmentInstance(child, fragmentInstance) { + // Noop + }, + scheduleTimeout: setTimeout, cancelTimeout: clearTimeout, noTimeout: -1, diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 9ced8912b977b..6af8c1356f9ca 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -47,6 +47,7 @@ import isArray from 'shared/isArray'; import { enableAsyncIterableChildren, disableLegacyMode, + enableFragmentRefs, } from 'shared/ReactFeatureFlags'; import { @@ -214,10 +215,14 @@ function validateFragmentProps( const keys = Object.keys(element.props); for (let i = 0; i < keys.length; i++) { const key = keys[i]; - if (key !== 'children' && key !== 'key') { + if ( + key !== 'children' && + key !== 'key' && + (enableFragmentRefs ? key !== 'ref' : true) + ) { if (fiber === null) { - // For unkeyed root fragments there's no Fiber. We create a fake one just for - // error stack handling. + // For unkeyed root fragments without refs (enableFragmentRefs), + // there's no Fiber. We create a fake one just for error stack handling. fiber = createFiberFromElement(element, returnFiber.mode, 0); if (__DEV__) { fiber._debugInfo = currentDebugInfo; @@ -227,11 +232,19 @@ function validateFragmentProps( runWithFiberInDEV( fiber, erroredKey => { - console.error( - 'Invalid prop `%s` supplied to `React.Fragment`. ' + - 'React.Fragment can only have `key` and `children` props.', - erroredKey, - ); + if (enableFragmentRefs) { + console.error( + 'Invalid prop `%s` supplied to `React.Fragment`. ' + + 'React.Fragment can only have `key`, `ref`, and `children` props.', + erroredKey, + ); + } else { + console.error( + 'Invalid prop `%s` supplied to `React.Fragment`. ' + + 'React.Fragment can only have `key` and `children` props.', + erroredKey, + ); + } }, key, ); @@ -517,6 +530,9 @@ function createChildReconciler( lanes, element.key, ); + if (enableFragmentRefs) { + coerceRef(updated, element); + } validateFragmentProps(element, updated, returnFiber); return updated; } @@ -1619,6 +1635,9 @@ function createChildReconciler( if (child.tag === Fragment) { deleteRemainingChildren(returnFiber, child.sibling); const existing = useFiber(child, element.props.children); + if (enableFragmentRefs) { + coerceRef(existing, element); + } existing.return = returnFiber; if (__DEV__) { existing._debugOwner = element._owner; @@ -1670,6 +1689,9 @@ function createChildReconciler( lanes, element.key, ); + if (enableFragmentRefs) { + coerceRef(created, element); + } created.return = returnFiber; if (__DEV__) { // We treat the parent as the owner for stack purposes. @@ -1742,17 +1764,19 @@ function createChildReconciler( // not as a fragment. Nested arrays on the other hand will be treated as // fragment nodes. Recursion happens at the normal flow. - // Handle top level unkeyed fragments as if they were arrays. - // This leads to an ambiguity between <>{[...]} and <>.... + // Handle top level unkeyed fragments without refs (enableFragmentRefs) + // as if they were arrays. This leads to an ambiguity between <>{[...]} and <>.... // We treat the ambiguous cases above the same. // We don't use recursion here because a fragment inside a fragment // is no longer considered "top level" for these purposes. - const isUnkeyedTopLevelFragment = + const isUnkeyedUnrefedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && - newChild.key === null; - if (isUnkeyedTopLevelFragment) { + newChild.key === null && + (enableFragmentRefs ? newChild.props.ref === undefined : true); + + if (isUnkeyedUnrefedTopLevelFragment) { validateFragmentProps(newChild, null, returnFiber); newChild = newChild.props.children; } diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 9c88a5de870cf..b37bd4ea3f44f 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -116,6 +116,7 @@ import { disableDefaultPropsExceptForClasses, enableHydrationLaneScheduling, enableViewTransition, + enableFragmentRefs, } from 'shared/ReactFeatureFlags'; import isArray from 'shared/isArray'; import shallowEqual from 'shared/shallowEqual'; @@ -987,6 +988,9 @@ function updateFragment( renderLanes: Lanes, ) { const nextChildren = workInProgress.pendingProps; + if (enableFragmentRefs) { + markRef(current, workInProgress); + } reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; } diff --git a/packages/react-reconciler/src/ReactFiberCommitEffects.js b/packages/react-reconciler/src/ReactFiberCommitEffects.js index 05cf17a34273e..a0f6b54780a2d 100644 --- a/packages/react-reconciler/src/ReactFiberCommitEffects.js +++ b/packages/react-reconciler/src/ReactFiberCommitEffects.js @@ -11,6 +11,7 @@ import type {Fiber} from './ReactInternalTypes'; import type {UpdateQueue} from './ReactFiberClassUpdateQueue'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks'; import type {HookFlags} from './ReactHookEffectTags'; +import type {FragmentInstanceType} from './ReactFiberConfig'; import { getViewTransitionName, type ViewTransitionState, @@ -24,9 +25,11 @@ import { enableSchedulingProfiler, enableUseEffectCRUDOverload, enableViewTransition, + enableFragmentRefs, } from 'shared/ReactFeatureFlags'; import { ClassComponent, + Fragment, HostComponent, HostHoistable, HostSingleton, @@ -48,6 +51,7 @@ import { import { getPublicInstance, createViewTransitionInstance, + createFragmentInstance, } from './ReactFiberConfig'; import { captureCommitPhaseError, @@ -877,7 +881,7 @@ function commitAttachRef(finishedWork: Fiber) { case HostComponent: instanceToUse = getPublicInstance(finishedWork.stateNode); break; - case ViewTransitionComponent: + case ViewTransitionComponent: { if (enableViewTransition) { const instance: ViewTransitionState = finishedWork.stateNode; const props: ViewTransitionProps = finishedWork.memoizedProps; @@ -888,6 +892,18 @@ function commitAttachRef(finishedWork: Fiber) { instanceToUse = instance.ref; break; } + instanceToUse = finishedWork.stateNode; + break; + } + case Fragment: + if (enableFragmentRefs) { + const instance: null | FragmentInstanceType = finishedWork.stateNode; + if (instance === null) { + finishedWork.stateNode = createFragmentInstance(finishedWork); + } + instanceToUse = finishedWork.stateNode; + break; + } // Fallthrough default: instanceToUse = finishedWork.stateNode; diff --git a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js index c104c2a8464b5..7dad8b330d7e2 100644 --- a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js +++ b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js @@ -13,6 +13,7 @@ import type { SuspenseInstance, Container, ChildSet, + FragmentInstanceType, } from './ReactFiberConfig'; import type {Fiber, FiberRoot} from './ReactInternalTypes'; @@ -24,6 +25,7 @@ import { HostText, HostPortal, DehydratedFragment, + Fragment, } from './ReactWorkTags'; import {ContentReset, Placement} from './ReactFiberFlags'; import { @@ -50,11 +52,14 @@ import { acquireSingletonInstance, releaseSingletonInstance, isSingletonScope, + commitNewChildToFragmentInstance, + deleteChildFromFragmentInstance, } from './ReactFiberConfig'; import {captureCommitPhaseError} from './ReactFiberWorkLoop'; import {trackHostMutation} from './ReactFiberMutationTracking'; import {runWithFiberInDEV} from './ReactCurrentFiber'; +import {enableFragmentRefs} from 'shared/ReactFeatureFlags'; export function commitHostMount(finishedWork: Fiber) { const type = finishedWork.type; @@ -199,19 +204,46 @@ export function commitShowHideHostTextInstance(node: Fiber, isHidden: boolean) { } } -function getHostParentFiber(fiber: Fiber): Fiber { +export function commitNewChildToFragmentInstances( + fiber: Fiber, + parentFragmentInstances: Array, +): void { + for (let i = 0; i < parentFragmentInstances.length; i++) { + const fragmentInstance = parentFragmentInstances[i]; + commitNewChildToFragmentInstance(fiber.stateNode, fragmentInstance); + } +} + +export function commitFragmentInstanceInsertionEffects(fiber: Fiber): void { let parent = fiber.return; while (parent !== null) { + if (isFragmentInstanceParent(parent)) { + const fragmentInstance: FragmentInstanceType = parent.stateNode; + commitNewChildToFragmentInstance(fiber.stateNode, fragmentInstance); + } + if (isHostParent(parent)) { - return parent; + return; } + parent = parent.return; } +} - throw new Error( - 'Expected to find a host parent. This error is likely caused by a bug ' + - 'in React. Please file an issue.', - ); +export function commitFragmentInstanceDeletionEffects(fiber: Fiber): void { + let parent = fiber.return; + while (parent !== null) { + if (isFragmentInstanceParent(parent)) { + const fragmentInstance: FragmentInstanceType = parent.stateNode; + deleteChildFromFragmentInstance(fiber.stateNode, fragmentInstance); + } + + if (isHostParent(parent)) { + return; + } + + parent = parent.return; + } } function isHostParent(fiber: Fiber): boolean { @@ -226,6 +258,10 @@ function isHostParent(fiber: Fiber): boolean { ); } +function isFragmentInstanceParent(fiber: Fiber): boolean { + return fiber && fiber.tag === Fragment && fiber.stateNode !== null; +} + function getHostSibling(fiber: Fiber): ?Instance { // We're going to search forward into the tree until we find a sibling host // node. Unfortunately, if multiple insertions are done in a row we have to @@ -288,6 +324,7 @@ function insertOrAppendPlacementNodeIntoContainer( node: Fiber, before: ?Instance, parent: Container, + parentFragmentInstances: null | Array, ): void { const {tag} = node; const isHost = tag === HostComponent || tag === HostText; @@ -298,6 +335,16 @@ function insertOrAppendPlacementNodeIntoContainer( } else { appendChildToContainer(parent, stateNode); } + // TODO: Enable HostText for RN + if ( + enableFragmentRefs && + tag === HostComponent && + // Only run fragment insertion effects for initial insertions + node.alternate === null && + parentFragmentInstances !== null + ) { + commitNewChildToFragmentInstances(node, parentFragmentInstances); + } trackHostMutation(); return; } else if (tag === HostPortal) { @@ -319,10 +366,20 @@ function insertOrAppendPlacementNodeIntoContainer( const child = node.child; if (child !== null) { - insertOrAppendPlacementNodeIntoContainer(child, before, parent); + insertOrAppendPlacementNodeIntoContainer( + child, + before, + parent, + parentFragmentInstances, + ); let sibling = child.sibling; while (sibling !== null) { - insertOrAppendPlacementNodeIntoContainer(sibling, before, parent); + insertOrAppendPlacementNodeIntoContainer( + sibling, + before, + parent, + parentFragmentInstances, + ); sibling = sibling.sibling; } } @@ -332,6 +389,7 @@ function insertOrAppendPlacementNode( node: Fiber, before: ?Instance, parent: Instance, + parentFragmentInstances: null | Array, ): void { const {tag} = node; const isHost = tag === HostComponent || tag === HostText; @@ -342,6 +400,16 @@ function insertOrAppendPlacementNode( } else { appendChild(parent, stateNode); } + // TODO: Enable HostText for RN + if ( + enableFragmentRefs && + tag === HostComponent && + // Only run fragment insertion effects for initial insertions + node.alternate === null && + parentFragmentInstances !== null + ) { + commitNewChildToFragmentInstances(node, parentFragmentInstances); + } trackHostMutation(); return; } else if (tag === HostPortal) { @@ -362,10 +430,15 @@ function insertOrAppendPlacementNode( const child = node.child; if (child !== null) { - insertOrAppendPlacementNode(child, before, parent); + insertOrAppendPlacementNode(child, before, parent, parentFragmentInstances); let sibling = child.sibling; while (sibling !== null) { - insertOrAppendPlacementNode(sibling, before, parent); + insertOrAppendPlacementNode( + sibling, + before, + parent, + parentFragmentInstances, + ); sibling = sibling.sibling; } } @@ -377,40 +450,78 @@ function commitPlacement(finishedWork: Fiber): void { } // Recursively insert all host nodes into the parent. - const parentFiber = getHostParentFiber(finishedWork); + let hostParentFiber; + let parentFragmentInstances = null; + let parentFiber = finishedWork.return; + while (parentFiber !== null) { + if (enableFragmentRefs && isFragmentInstanceParent(parentFiber)) { + const fragmentInstance: FragmentInstanceType = parentFiber.stateNode; + if (parentFragmentInstances === null) { + parentFragmentInstances = [fragmentInstance]; + } else { + parentFragmentInstances.push(fragmentInstance); + } + } + if (isHostParent(parentFiber)) { + hostParentFiber = parentFiber; + break; + } + parentFiber = parentFiber.return; + } + if (hostParentFiber == null) { + throw new Error( + 'Expected to find a host parent. This error is likely caused by a bug ' + + 'in React. Please file an issue.', + ); + } - switch (parentFiber.tag) { + switch (hostParentFiber.tag) { case HostSingleton: { if (supportsSingletons) { - const parent: Instance = parentFiber.stateNode; + const parent: Instance = hostParentFiber.stateNode; const before = getHostSibling(finishedWork); // We only have the top Fiber that was inserted but we need to recurse down its // children to find all the terminal nodes. - insertOrAppendPlacementNode(finishedWork, before, parent); + insertOrAppendPlacementNode( + finishedWork, + before, + parent, + parentFragmentInstances, + ); break; } // Fall through } case HostComponent: { - const parent: Instance = parentFiber.stateNode; - if (parentFiber.flags & ContentReset) { + const parent: Instance = hostParentFiber.stateNode; + if (hostParentFiber.flags & ContentReset) { // Reset the text content of the parent before doing any insertions resetTextContent(parent); // Clear ContentReset from the effect tag - parentFiber.flags &= ~ContentReset; + hostParentFiber.flags &= ~ContentReset; } const before = getHostSibling(finishedWork); // We only have the top Fiber that was inserted but we need to recurse down its // children to find all the terminal nodes. - insertOrAppendPlacementNode(finishedWork, before, parent); + insertOrAppendPlacementNode( + finishedWork, + before, + parent, + parentFragmentInstances, + ); break; } case HostRoot: case HostPortal: { - const parent: Container = parentFiber.stateNode.containerInfo; + const parent: Container = hostParentFiber.stateNode.containerInfo; const before = getHostSibling(finishedWork); - insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent); + insertOrAppendPlacementNodeIntoContainer( + finishedWork, + before, + parent, + parentFragmentInstances, + ); break; } default: diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index b9ea97bf56d8c..8b82e6a7e713c 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -61,6 +61,7 @@ import { disableLegacyMode, enableComponentPerformanceTrack, enableViewTransition, + enableFragmentRefs, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -85,6 +86,7 @@ import { CacheComponent, TracingMarkerComponent, ViewTransitionComponent, + Fragment, } from './ReactWorkTags'; import { NoFlags, @@ -164,6 +166,7 @@ import { cancelRootViewTransitionName, restoreRootViewTransitionName, isSingletonScope, + updateFragmentInstanceFiber, } from './ReactFiberConfig'; import { captureCommitPhaseError, @@ -235,6 +238,8 @@ import { commitHostRemoveChild, commitHostSingletonAcquisition, commitHostSingletonRelease, + commitFragmentInstanceDeletionEffects, + commitFragmentInstanceInsertionEffects, } from './ReactFiberCommitHostEffects'; import { commitEnterViewTransitions, @@ -767,8 +772,15 @@ function commitLayoutEffectOnFiber( } break; } - // Fallthrough + break; } + case Fragment: + if (enableFragmentRefs) { + if (flags & Ref) { + safelyAttachRef(finishedWork, finishedWork.return); + } + } + // Fallthrough default: { recursivelyTraverseLayoutEffects( finishedRoot, @@ -1353,6 +1365,9 @@ function commitDeletionEffectsOnFiber( if (!offscreenSubtreeWasHidden) { safelyDetachRef(deletedFiber, nearestMountedAncestor); } + if (enableFragmentRefs && deletedFiber.tag === HostComponent) { + commitFragmentInstanceDeletionEffects(deletedFiber); + } // Intentional fallthrough to next branch } case HostText: { @@ -1563,6 +1578,14 @@ function commitDeletionEffectsOnFiber( } break; } + case Fragment: { + if (enableFragmentRefs) { + if (!offscreenSubtreeWasHidden) { + safelyDetachRef(deletedFiber, nearestMountedAncestor); + } + } + // Fallthrough + } default: { recursivelyTraverseDeletionEffects( finishedRoot, @@ -1947,6 +1970,7 @@ function commitMutationEffectsOnFiber( } case HostComponent: { recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork, lanes); if (flags & Ref) { @@ -2270,7 +2294,7 @@ function commitMutationEffectsOnFiber( } break; } - case ViewTransitionComponent: + case ViewTransitionComponent: { if (enableViewTransition) { if (flags & Ref) { if (!offscreenSubtreeWasHidden && current !== null) { @@ -2298,7 +2322,8 @@ function commitMutationEffectsOnFiber( popMutationContext(prevMutationContext); break; } - // Fallthrough + break; + } case ScopeComponent: { if (enableScopeAPI) { recursivelyTraverseMutationEffects(root, finishedWork, lanes); @@ -2321,6 +2346,13 @@ function commitMutationEffectsOnFiber( } break; } + case Fragment: + if (enableFragmentRefs) { + if (current && current.stateNode !== null) { + updateFragmentInstanceFiber(finishedWork, current.stateNode); + } + } + // Fallthrough default: { recursivelyTraverseMutationEffects(root, finishedWork, lanes); commitReconciliationEffects(finishedWork, lanes); @@ -2638,6 +2670,10 @@ export function disappearLayoutEffects(finishedWork: Fiber) { // TODO (Offscreen) Check: flags & RefStatic safelyDetachRef(finishedWork, finishedWork.return); + if (enableFragmentRefs && finishedWork.tag === HostComponent) { + commitFragmentInstanceDeletionEffects(finishedWork); + } + recursivelyTraverseDisappearLayoutEffects(finishedWork); break; } @@ -2658,6 +2694,13 @@ export function disappearLayoutEffects(finishedWork: Fiber) { if (enableViewTransition) { safelyDetachRef(finishedWork, finishedWork.return); } + recursivelyTraverseDisappearLayoutEffects(finishedWork); + break; + } + case Fragment: { + if (enableFragmentRefs) { + safelyDetachRef(finishedWork, finishedWork.return); + } // Fallthrough } default: { @@ -2765,6 +2808,10 @@ export function reappearLayoutEffects( } case HostHoistable: case HostComponent: { + // TODO: Enable HostText for RN + if (enableFragmentRefs && finishedWork.tag === HostComponent) { + commitFragmentInstanceInsertionEffects(finishedWork); + } recursivelyTraverseReappearLayoutEffects( finishedRoot, finishedWork, @@ -2857,6 +2904,12 @@ export function reappearLayoutEffects( safelyAttachRef(finishedWork, finishedWork.return); break; } + break; + } + case Fragment: { + if (enableFragmentRefs) { + safelyAttachRef(finishedWork, finishedWork.return); + } // Fallthrough } default: { diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js index e978249e185fe..2be7d18b87caf 100644 --- a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js @@ -45,6 +45,7 @@ export type ViewTransitionInstance = null | {name: string, ...}; export opaque type InstanceMeasurement = mixed; export type EventResponder = any; export type GestureTimeline = any; +export type FragmentInstanceType = null; export const rendererVersion = $$$config.rendererVersion; export const rendererPackageName = $$$config.rendererPackageName; @@ -160,6 +161,13 @@ export const subscribeToGestureDirection = export const createViewTransitionInstance = $$$config.createViewTransitionInstance; export const clearContainer = $$$config.clearContainer; +export const createFragmentInstance = $$$config.createFragmentInstance; +export const updateFragmentInstanceFiber = + $$$config.updateFragmentInstanceFiber; +export const commitNewChildToFragmentInstance = + $$$config.commitNewChildToFragmentInstance; +export const deleteChildFromFragmentInstance = + $$$config.deleteChildFromFragmentInstance; // ------------------- // Persistence diff --git a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js index bb18e44e5580d..6ac2777c92662 100644 --- a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js +++ b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js @@ -447,6 +447,35 @@ export function createViewTransitionInstance( return null; } +export type FragmentInstanceType = null; + +export function createFragmentInstance( + fragmentFiber: Object, +): FragmentInstanceType { + return null; +} + +export function updateFragmentInstanceFiber( + fragmentFiber: Object, + instance: FragmentInstanceType, +): void { + // Noop +} + +export function commitNewChildToFragmentInstance( + child: Instance, + fragmentInstance: FragmentInstanceType, +): void { + // noop +} + +export function deleteChildFromFragmentInstance( + child: Instance, + fragmentInstance: FragmentInstanceType, +): void { + // Noop +} + export function getInstanceFromNode(mockNode: Object): Object | null { const instance = nodeToInstanceMap.get(mockNode); if (instance !== undefined) { diff --git a/packages/react/src/__tests__/ReactElementValidator-test.internal.js b/packages/react/src/__tests__/ReactElementValidator-test.internal.js index bbb06411b03e2..f1ee7026346ec 100644 --- a/packages/react/src/__tests__/ReactElementValidator-test.internal.js +++ b/packages/react/src/__tests__/ReactElementValidator-test.internal.js @@ -427,9 +427,13 @@ describe('ReactElementValidator', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); await act(() => root.render(React.createElement(Foo))); assertConsoleErrorDev([ - 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' + - 'can only have `key` and `children` props.\n' + - ' in Foo (at **)', + gate('enableFragmentRefs') + ? 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' + + 'can only have `key`, `ref`, and `children` props.\n' + + ' in Foo (at **)' + : 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' + + 'can only have `key` and `children` props.\n' + + ' in Foo (at **)', ]); }); diff --git a/packages/react/src/__tests__/ReactJSXElementValidator-test.js b/packages/react/src/__tests__/ReactJSXElementValidator-test.js index 041191e2ab5fd..41ee720478ae1 100644 --- a/packages/react/src/__tests__/ReactJSXElementValidator-test.js +++ b/packages/react/src/__tests__/ReactJSXElementValidator-test.js @@ -221,9 +221,13 @@ describe('ReactJSXElementValidator', () => { root.render(); }); assertConsoleErrorDev([ - 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' + - 'can only have `key` and `children` props.\n' + - ' in Foo (at **)', + gate('enableFragmentRefs') + ? 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' + + 'can only have `key`, `ref`, and `children` props.\n' + + ' in Foo (at **)' + : 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' + + 'can only have `key` and `children` props.\n' + + ' in Foo (at **)', ]); }); @@ -246,11 +250,15 @@ describe('ReactJSXElementValidator', () => { await act(() => { root.render(); }); - assertConsoleErrorDev([ - 'Invalid prop `ref` supplied to `React.Fragment`.' + - ' React.Fragment can only have `key` and `children` props.\n' + - ' in Foo (at **)', - ]); + assertConsoleErrorDev( + gate('enableFragmentRefs') + ? [] + : [ + 'Invalid prop `ref` supplied to `React.Fragment`.' + + ' React.Fragment can only have `key` and `children` props.\n' + + ' in Foo (at **)', + ], + ); }); it('does not warn for fragments of multiple elements without keys', async () => { diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 5588004bdf278..d50965f13e949 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -160,9 +160,10 @@ export const enableInfiniteRenderLoopDetection = false; export const enableUseEffectCRUDOverload = false; export const enableFastAddPropertiesInDiffing = true; - export const enableLazyPublicInstanceInFabric = false; +export const enableFragmentRefs = false; + // ----------------------------------------------------------------------------- // Ready for next major. // diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index f996274b86a18..f49f972e59981 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -83,6 +83,7 @@ export const enableThrottledScheduling = false; export const enableViewTransition = false; export const enableSwipeTransition = false; export const enableScrollEndPolyfill = true; +export const enableFragmentRefs = false; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 9b878754736ce..baeef0b56483d 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -76,6 +76,8 @@ export const enableFastAddPropertiesInDiffing = false; export const enableLazyPublicInstanceInFabric = false; export const enableScrollEndPolyfill = true; +export const enableFragmentRefs = false; + // Profiling Only export const enableProfilerTimer = __PROFILE__; export const enableProfilerCommitHooks = __PROFILE__; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index f5deddae739be..1e3eedc9e2255 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -76,6 +76,8 @@ export const enableFastAddPropertiesInDiffing = true; export const enableLazyPublicInstanceInFabric = false; export const enableScrollEndPolyfill = true; +export const enableFragmentRefs = false; + // TODO: This must be in sync with the main ReactFeatureFlags file because // the Test Renderer's value must be the same as the one used by the // react package. diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index 4df92a5c90ef8..f2fa119800fa8 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -71,6 +71,7 @@ export const enableSwipeTransition = false; export const enableFastAddPropertiesInDiffing = false; export const enableLazyPublicInstanceInFabric = false; export const enableScrollEndPolyfill = true; +export const enableFragmentRefs = false; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index b15f54484b029..2342b959f99f8 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -87,5 +87,7 @@ export const enableFastAddPropertiesInDiffing = false; export const enableLazyPublicInstanceInFabric = false; export const enableScrollEndPolyfill = true; +export const enableFragmentRefs = false; + // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 3fe3b7a0a3e29..0116e160b643e 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -41,6 +41,7 @@ export const enableLazyPublicInstanceInFabric = false; export const enableViewTransition = __VARIANT__; export const enableComponentPerformanceTrack = __VARIANT__; export const enableScrollEndPolyfill = __VARIANT__; +export const enableFragmentRefs = __VARIANT__; // TODO: These flags are hard-coded to the default values used in open source. // Update the tests so that they pass in either mode, then set these diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 07e7e1f51aa17..0584af1f81826 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -39,6 +39,7 @@ export const { enableViewTransition, enableComponentPerformanceTrack, enableScrollEndPolyfill, + enableFragmentRefs, } = dynamicFeatureFlags; // On WWW, __EXPERIMENTAL__ is used for a new modern build.