diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index 151cd3c8cc2ae..1cf98193abcc7 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -24,6 +24,7 @@ import { } from 'react-reconciler/src/ReactEventPriorities'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import {HostText} from 'react-reconciler/src/ReactWorkTags'; +import {traverseFragmentInstance} from 'react-reconciler/src/ReactFiberTreeReflection'; // Modules provided by RN: import { @@ -622,30 +623,91 @@ export function waitForCommitToBeReady(): null { return null; } -export type FragmentInstanceType = null; +export type FragmentInstanceType = { + _fragmentFiber: Fiber, + _observers: null | Set, + observeUsing: (observer: IntersectionObserver) => void, + unobserveUsing: (observer: IntersectionObserver) => void, +}; + +function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) { + this._fragmentFiber = fragmentFiber; + this._observers = null; +} + +// $FlowFixMe[prop-missing] +FragmentInstance.prototype.observeUsing = function ( + this: FragmentInstanceType, + observer: IntersectionObserver, +): void { + if (this._observers === null) { + this._observers = new Set(); + } + this._observers.add(observer); + traverseFragmentInstance(this._fragmentFiber, observeChild, observer); +}; +function observeChild(instance: Instance, observer: IntersectionObserver) { + const publicInstance = getPublicInstance(instance); + if (publicInstance == null) { + throw new Error('Expected to find a host node. This is a bug in React.'); + } + // $FlowFixMe[incompatible-call] Element types are behind a flag in RN + observer.observe(publicInstance); + return false; +} +// $FlowFixMe[prop-missing] +FragmentInstance.prototype.unobserveUsing = function ( + this: FragmentInstanceType, + observer: IntersectionObserver, +): void { + if (this._observers === null || !this._observers.has(observer)) { + if (__DEV__) { + console.error( + 'You are calling unobserveUsing() with an observer that is not being observed with this fragment ' + + 'instance. First attach the observer with observeUsing()', + ); + } + } else { + this._observers.delete(observer); + traverseFragmentInstance(this._fragmentFiber, unobserveChild, observer); + } +}; +function unobserveChild(instance: Instance, observer: IntersectionObserver) { + const publicInstance = getPublicInstance(instance); + if (publicInstance == null) { + throw new Error('Expected to find a host node. This is a bug in React.'); + } + // $FlowFixMe[incompatible-call] Element types are behind a flag in RN + observer.unobserve(publicInstance); + return false; +} export function createFragmentInstance( fragmentFiber: Fiber, ): FragmentInstanceType { - return null; + return new (FragmentInstance: any)(fragmentFiber); } export function updateFragmentInstanceFiber( fragmentFiber: Fiber, instance: FragmentInstanceType, ): void { - // Noop + instance._fragmentFiber = fragmentFiber; } export function commitNewChildToFragmentInstance( - child: PublicInstance, + child: Instance, fragmentInstance: FragmentInstanceType, ): void { - // Noop + if (fragmentInstance._observers !== null) { + fragmentInstance._observers.forEach(observer => { + observeChild(child, observer); + }); + } } export function deleteChildFromFragmentInstance( - child: PublicInstance, + child: Instance, fragmentInstance: FragmentInstanceType, ): void { // Noop diff --git a/packages/react-native-renderer/src/__tests__/ReactFabricFragmentRefs-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabricFragmentRefs-test.internal.js new file mode 100644 index 0000000000000..725b8d9de694f --- /dev/null +++ b/packages/react-native-renderer/src/__tests__/ReactFabricFragmentRefs-test.internal.js @@ -0,0 +1,83 @@ +/** + * 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 react-core + * @jest-environment node + */ + +'use strict'; + +let React; +let ReactFabric; +let createReactNativeComponentClass; +let act; +let View; +let Text; + +describe('Fabric FragmentRefs', () => { + beforeEach(() => { + jest.resetModules(); + + require('react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager'); + + React = require('react'); + ReactFabric = require('react-native-renderer/fabric'); + createReactNativeComponentClass = + require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface') + .ReactNativeViewConfigRegistry.register; + ({act} = require('internal-test-utils')); + View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {nativeID: true}, + uiViewClassName: 'RCTView', + })); + Text = createReactNativeComponentClass('RCTText', () => ({ + validAttributes: {nativeID: true}, + uiViewClassName: 'RCTText', + })); + }); + + // @gate enableFragmentRefs + it('attaches a ref to Fragment', async () => { + const fragmentRef = React.createRef(); + + await act(() => + ReactFabric.render( + + + + Hi + + + , + 11, + null, + true, + ), + ); + + expect(fragmentRef.current).not.toBe(null); + }); + + // @gate enableFragmentRefs + it('accepts a ref callback', async () => { + let fragmentRef; + + await act(() => { + ReactFabric.render( + (fragmentRef = ref)}> + + Hi + + , + 11, + null, + true, + ); + }); + + expect(fragmentRef && fragmentRef._fragmentFiber).toBeTruthy(); + }); +}); diff --git a/packages/react-reconciler/src/ReactFiberTreeReflection.js b/packages/react-reconciler/src/ReactFiberTreeReflection.js index d682784f9a2aa..d799e2308ae47 100644 --- a/packages/react-reconciler/src/ReactFiberTreeReflection.js +++ b/packages/react-reconciler/src/ReactFiberTreeReflection.js @@ -345,9 +345,9 @@ export function doesFiberContain( return false; } -export function traverseFragmentInstance( +export function traverseFragmentInstance( fragmentFiber: Fiber, - fn: (Instance, A, B, C) => boolean, + fn: (I, A, B, C) => boolean, a: A, b: B, c: C, @@ -355,9 +355,9 @@ export function traverseFragmentInstance( traverseFragmentInstanceChildren(fragmentFiber.child, fn, a, b, c); } -function traverseFragmentInstanceChildren( +function traverseFragmentInstanceChildren( child: Fiber | null, - fn: (Instance, A, B, C) => boolean, + fn: (I, A, B, C) => boolean, a: A, b: B, c: C, diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js b/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js index e80b745587ba5..f1ced67c446d9 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js @@ -28,3 +28,4 @@ export const enableSiblingPrerendering = __VARIANT__; export const enableFastAddPropertiesInDiffing = __VARIANT__; export const enableLazyPublicInstanceInFabric = __VARIANT__; export const renameElementSymbol = __VARIANT__; +export const enableFragmentRefs = __VARIANT__; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index a13ae59e80ac5..6bc3f7b1d1e1f 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -30,6 +30,7 @@ export const { enableFastAddPropertiesInDiffing, enableLazyPublicInstanceInFabric, renameElementSymbol, + enableFragmentRefs, } = dynamicFlags; // The rest of the flags are static for better dead code elimination. @@ -84,7 +85,6 @@ export const enableGestureTransition = false; export const enableScrollEndPolyfill = true; export const enableSuspenseyImages = false; export const enableSrcObject = false; -export const enableFragmentRefs = false; export const ownerStackLimit = 1e4; // Flow magic to verify the exports of this file match the original version. diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 25e0ee802442d..d81f7489f99f6 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -543,5 +543,6 @@ "555": "Cannot requestFormReset() inside a startGestureTransition. There should be no side-effects associated with starting a Gesture until its Action is invoked. Move side-effects to the Action instead.", "556": "Expected prepareToHydrateHostActivityInstance() to never be called. This error is likely caused by a bug in React. Please file an issue.", "557": "Expected to have a hydrated activity instance. This error is likely caused by a bug in React. Please file an issue.", - "558": "Client rendering an Activity suspended it again. This is a bug in React." + "558": "Client rendering an Activity suspended it again. This is a bug in React.", + "559": "Expected to find a host node. This is a bug in React." }