Skip to content

Commit 408d055

Browse files
authored
Add Fragment Refs to Fabric with intersection observer support (facebook#33056)
Adds Fragment Ref support to RN through the Fabric config, starting with `observeUsing`/`unobserveUsing`. This is mostly a copy from the implementation on DOM, and some of it can likely be shared in the future but keeping it separate for now and we can refactor as we add more features. Added a basic test with Fabric, but testing specific methods requires so much mocking that it doesn't seem valuable here. I built Fabric and ran on the Catalyst app internally to test with intersection observers end to end.
1 parent fbf29cc commit 408d055

File tree

6 files changed

+159
-12
lines changed

6 files changed

+159
-12
lines changed

packages/react-native-renderer/src/ReactFiberConfigFabric.js

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from 'react-reconciler/src/ReactEventPriorities';
2525
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
2626
import {HostText} from 'react-reconciler/src/ReactWorkTags';
27+
import {traverseFragmentInstance} from 'react-reconciler/src/ReactFiberTreeReflection';
2728

2829
// Modules provided by RN:
2930
import {
@@ -622,30 +623,91 @@ export function waitForCommitToBeReady(): null {
622623
return null;
623624
}
624625

625-
export type FragmentInstanceType = null;
626+
export type FragmentInstanceType = {
627+
_fragmentFiber: Fiber,
628+
_observers: null | Set<IntersectionObserver>,
629+
observeUsing: (observer: IntersectionObserver) => void,
630+
unobserveUsing: (observer: IntersectionObserver) => void,
631+
};
632+
633+
function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) {
634+
this._fragmentFiber = fragmentFiber;
635+
this._observers = null;
636+
}
637+
638+
// $FlowFixMe[prop-missing]
639+
FragmentInstance.prototype.observeUsing = function (
640+
this: FragmentInstanceType,
641+
observer: IntersectionObserver,
642+
): void {
643+
if (this._observers === null) {
644+
this._observers = new Set();
645+
}
646+
this._observers.add(observer);
647+
traverseFragmentInstance(this._fragmentFiber, observeChild, observer);
648+
};
649+
function observeChild(instance: Instance, observer: IntersectionObserver) {
650+
const publicInstance = getPublicInstance(instance);
651+
if (publicInstance == null) {
652+
throw new Error('Expected to find a host node. This is a bug in React.');
653+
}
654+
// $FlowFixMe[incompatible-call] Element types are behind a flag in RN
655+
observer.observe(publicInstance);
656+
return false;
657+
}
658+
// $FlowFixMe[prop-missing]
659+
FragmentInstance.prototype.unobserveUsing = function (
660+
this: FragmentInstanceType,
661+
observer: IntersectionObserver,
662+
): void {
663+
if (this._observers === null || !this._observers.has(observer)) {
664+
if (__DEV__) {
665+
console.error(
666+
'You are calling unobserveUsing() with an observer that is not being observed with this fragment ' +
667+
'instance. First attach the observer with observeUsing()',
668+
);
669+
}
670+
} else {
671+
this._observers.delete(observer);
672+
traverseFragmentInstance(this._fragmentFiber, unobserveChild, observer);
673+
}
674+
};
675+
function unobserveChild(instance: Instance, observer: IntersectionObserver) {
676+
const publicInstance = getPublicInstance(instance);
677+
if (publicInstance == null) {
678+
throw new Error('Expected to find a host node. This is a bug in React.');
679+
}
680+
// $FlowFixMe[incompatible-call] Element types are behind a flag in RN
681+
observer.unobserve(publicInstance);
682+
return false;
683+
}
626684

627685
export function createFragmentInstance(
628686
fragmentFiber: Fiber,
629687
): FragmentInstanceType {
630-
return null;
688+
return new (FragmentInstance: any)(fragmentFiber);
631689
}
632690

633691
export function updateFragmentInstanceFiber(
634692
fragmentFiber: Fiber,
635693
instance: FragmentInstanceType,
636694
): void {
637-
// Noop
695+
instance._fragmentFiber = fragmentFiber;
638696
}
639697

640698
export function commitNewChildToFragmentInstance(
641-
child: PublicInstance,
699+
child: Instance,
642700
fragmentInstance: FragmentInstanceType,
643701
): void {
644-
// Noop
702+
if (fragmentInstance._observers !== null) {
703+
fragmentInstance._observers.forEach(observer => {
704+
observeChild(child, observer);
705+
});
706+
}
645707
}
646708

647709
export function deleteChildFromFragmentInstance(
648-
child: PublicInstance,
710+
child: Instance,
649711
fragmentInstance: FragmentInstanceType,
650712
): void {
651713
// Noop
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
* @jest-environment node
9+
*/
10+
11+
'use strict';
12+
13+
let React;
14+
let ReactFabric;
15+
let createReactNativeComponentClass;
16+
let act;
17+
let View;
18+
let Text;
19+
20+
describe('Fabric FragmentRefs', () => {
21+
beforeEach(() => {
22+
jest.resetModules();
23+
24+
require('react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager');
25+
26+
React = require('react');
27+
ReactFabric = require('react-native-renderer/fabric');
28+
createReactNativeComponentClass =
29+
require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface')
30+
.ReactNativeViewConfigRegistry.register;
31+
({act} = require('internal-test-utils'));
32+
View = createReactNativeComponentClass('RCTView', () => ({
33+
validAttributes: {nativeID: true},
34+
uiViewClassName: 'RCTView',
35+
}));
36+
Text = createReactNativeComponentClass('RCTText', () => ({
37+
validAttributes: {nativeID: true},
38+
uiViewClassName: 'RCTText',
39+
}));
40+
});
41+
42+
// @gate enableFragmentRefs
43+
it('attaches a ref to Fragment', async () => {
44+
const fragmentRef = React.createRef();
45+
46+
await act(() =>
47+
ReactFabric.render(
48+
<View>
49+
<React.Fragment ref={fragmentRef}>
50+
<View>
51+
<Text>Hi</Text>
52+
</View>
53+
</React.Fragment>
54+
</View>,
55+
11,
56+
null,
57+
true,
58+
),
59+
);
60+
61+
expect(fragmentRef.current).not.toBe(null);
62+
});
63+
64+
// @gate enableFragmentRefs
65+
it('accepts a ref callback', async () => {
66+
let fragmentRef;
67+
68+
await act(() => {
69+
ReactFabric.render(
70+
<React.Fragment ref={ref => (fragmentRef = ref)}>
71+
<View nativeID="child">
72+
<Text>Hi</Text>
73+
</View>
74+
</React.Fragment>,
75+
11,
76+
null,
77+
true,
78+
);
79+
});
80+
81+
expect(fragmentRef && fragmentRef._fragmentFiber).toBeTruthy();
82+
});
83+
});

packages/react-reconciler/src/ReactFiberTreeReflection.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -345,19 +345,19 @@ export function doesFiberContain(
345345
return false;
346346
}
347347

348-
export function traverseFragmentInstance<A, B, C>(
348+
export function traverseFragmentInstance<I, A, B, C>(
349349
fragmentFiber: Fiber,
350-
fn: (Instance, A, B, C) => boolean,
350+
fn: (I, A, B, C) => boolean,
351351
a: A,
352352
b: B,
353353
c: C,
354354
): void {
355355
traverseFragmentInstanceChildren(fragmentFiber.child, fn, a, b, c);
356356
}
357357

358-
function traverseFragmentInstanceChildren<A, B, C>(
358+
function traverseFragmentInstanceChildren<I, A, B, C>(
359359
child: Fiber | null,
360-
fn: (Instance, A, B, C) => boolean,
360+
fn: (I, A, B, C) => boolean,
361361
a: A,
362362
b: B,
363363
c: C,

packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ export const enableSiblingPrerendering = __VARIANT__;
2828
export const enableFastAddPropertiesInDiffing = __VARIANT__;
2929
export const enableLazyPublicInstanceInFabric = __VARIANT__;
3030
export const renameElementSymbol = __VARIANT__;
31+
export const enableFragmentRefs = __VARIANT__;

packages/shared/forks/ReactFeatureFlags.native-fb.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const {
3030
enableFastAddPropertiesInDiffing,
3131
enableLazyPublicInstanceInFabric,
3232
renameElementSymbol,
33+
enableFragmentRefs,
3334
} = dynamicFlags;
3435

3536
// The rest of the flags are static for better dead code elimination.
@@ -84,7 +85,6 @@ export const enableGestureTransition = false;
8485
export const enableScrollEndPolyfill = true;
8586
export const enableSuspenseyImages = false;
8687
export const enableSrcObject = false;
87-
export const enableFragmentRefs = false;
8888
export const ownerStackLimit = 1e4;
8989

9090
// Flow magic to verify the exports of this file match the original version.

scripts/error-codes/codes.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -543,5 +543,6 @@
543543
"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.",
544544
"556": "Expected prepareToHydrateHostActivityInstance() to never be called. This error is likely caused by a bug in React. Please file an issue.",
545545
"557": "Expected to have a hydrated activity instance. This error is likely caused by a bug in React. Please file an issue.",
546-
"558": "Client rendering an Activity suspended it again. This is a bug in React."
546+
"558": "Client rendering an Activity suspended it again. This is a bug in React.",
547+
"559": "Expected to find a host node. This is a bug in React."
547548
}

0 commit comments

Comments
 (0)