Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 68 additions & 6 deletions packages/react-native-renderer/src/ReactFiberConfigFabric.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -622,30 +623,91 @@ export function waitForCommitToBeReady(): null {
return null;
}

export type FragmentInstanceType = null;
export type FragmentInstanceType = {
_fragmentFiber: Fiber,
_observers: null | Set<IntersectionObserver>,
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
<View>
<React.Fragment ref={fragmentRef}>
<View>
<Text>Hi</Text>
</View>
</React.Fragment>
</View>,
11,
null,
true,
),
);

expect(fragmentRef.current).not.toBe(null);
});

// @gate enableFragmentRefs
it('accepts a ref callback', async () => {
let fragmentRef;

await act(() => {
ReactFabric.render(
<React.Fragment ref={ref => (fragmentRef = ref)}>
<View nativeID="child">
<Text>Hi</Text>
</View>
</React.Fragment>,
11,
null,
true,
);
});

expect(fragmentRef && fragmentRef._fragmentFiber).toBeTruthy();
});
});
8 changes: 4 additions & 4 deletions packages/react-reconciler/src/ReactFiberTreeReflection.js
Original file line number Diff line number Diff line change
Expand Up @@ -345,19 +345,19 @@ export function doesFiberContain(
return false;
}

export function traverseFragmentInstance<A, B, C>(
export function traverseFragmentInstance<I, A, B, C>(
fragmentFiber: Fiber,
fn: (Instance, A, B, C) => boolean,
fn: (I, A, B, C) => boolean,
a: A,
b: B,
c: C,
): void {
traverseFragmentInstanceChildren(fragmentFiber.child, fn, a, b, c);
}

function traverseFragmentInstanceChildren<A, B, C>(
function traverseFragmentInstanceChildren<I, A, B, C>(
child: Fiber | null,
fn: (Instance, A, B, C) => boolean,
fn: (I, A, B, C) => boolean,
a: A,
b: B,
c: C,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ export const enableSiblingPrerendering = __VARIANT__;
export const enableFastAddPropertiesInDiffing = __VARIANT__;
export const enableLazyPublicInstanceInFabric = __VARIANT__;
export const renameElementSymbol = __VARIANT__;
export const enableFragmentRefs = __VARIANT__;
2 changes: 1 addition & 1 deletion packages/shared/forks/ReactFeatureFlags.native-fb.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const {
enableFastAddPropertiesInDiffing,
enableLazyPublicInstanceInFabric,
renameElementSymbol,
enableFragmentRefs,
} = dynamicFlags;

// The rest of the flags are static for better dead code elimination.
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion scripts/error-codes/codes.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
Loading