mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
Add Fragment Refs to Fabric with intersection observer support (#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.
This commit is contained in:
parent
fbf29ccaa3
commit
408d055a3b
|
|
@ -24,6 +24,7 @@ import {
|
||||||
} from 'react-reconciler/src/ReactEventPriorities';
|
} from 'react-reconciler/src/ReactEventPriorities';
|
||||||
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
|
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
|
||||||
import {HostText} from 'react-reconciler/src/ReactWorkTags';
|
import {HostText} from 'react-reconciler/src/ReactWorkTags';
|
||||||
|
import {traverseFragmentInstance} from 'react-reconciler/src/ReactFiberTreeReflection';
|
||||||
|
|
||||||
// Modules provided by RN:
|
// Modules provided by RN:
|
||||||
import {
|
import {
|
||||||
|
|
@ -622,30 +623,91 @@ export function waitForCommitToBeReady(): null {
|
||||||
return 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(
|
export function createFragmentInstance(
|
||||||
fragmentFiber: Fiber,
|
fragmentFiber: Fiber,
|
||||||
): FragmentInstanceType {
|
): FragmentInstanceType {
|
||||||
return null;
|
return new (FragmentInstance: any)(fragmentFiber);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateFragmentInstanceFiber(
|
export function updateFragmentInstanceFiber(
|
||||||
fragmentFiber: Fiber,
|
fragmentFiber: Fiber,
|
||||||
instance: FragmentInstanceType,
|
instance: FragmentInstanceType,
|
||||||
): void {
|
): void {
|
||||||
// Noop
|
instance._fragmentFiber = fragmentFiber;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function commitNewChildToFragmentInstance(
|
export function commitNewChildToFragmentInstance(
|
||||||
child: PublicInstance,
|
child: Instance,
|
||||||
fragmentInstance: FragmentInstanceType,
|
fragmentInstance: FragmentInstanceType,
|
||||||
): void {
|
): void {
|
||||||
// Noop
|
if (fragmentInstance._observers !== null) {
|
||||||
|
fragmentInstance._observers.forEach(observer => {
|
||||||
|
observeChild(child, observer);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteChildFromFragmentInstance(
|
export function deleteChildFromFragmentInstance(
|
||||||
child: PublicInstance,
|
child: Instance,
|
||||||
fragmentInstance: FragmentInstanceType,
|
fragmentInstance: FragmentInstanceType,
|
||||||
): void {
|
): void {
|
||||||
// Noop
|
// Noop
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -345,9 +345,9 @@ export function doesFiberContain(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function traverseFragmentInstance<A, B, C>(
|
export function traverseFragmentInstance<I, A, B, C>(
|
||||||
fragmentFiber: Fiber,
|
fragmentFiber: Fiber,
|
||||||
fn: (Instance, A, B, C) => boolean,
|
fn: (I, A, B, C) => boolean,
|
||||||
a: A,
|
a: A,
|
||||||
b: B,
|
b: B,
|
||||||
c: C,
|
c: C,
|
||||||
|
|
@ -355,9 +355,9 @@ export function traverseFragmentInstance<A, B, C>(
|
||||||
traverseFragmentInstanceChildren(fragmentFiber.child, fn, a, b, c);
|
traverseFragmentInstanceChildren(fragmentFiber.child, fn, a, b, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
function traverseFragmentInstanceChildren<A, B, C>(
|
function traverseFragmentInstanceChildren<I, A, B, C>(
|
||||||
child: Fiber | null,
|
child: Fiber | null,
|
||||||
fn: (Instance, A, B, C) => boolean,
|
fn: (I, A, B, C) => boolean,
|
||||||
a: A,
|
a: A,
|
||||||
b: B,
|
b: B,
|
||||||
c: C,
|
c: C,
|
||||||
|
|
|
||||||
|
|
@ -28,3 +28,4 @@ export const enableSiblingPrerendering = __VARIANT__;
|
||||||
export const enableFastAddPropertiesInDiffing = __VARIANT__;
|
export const enableFastAddPropertiesInDiffing = __VARIANT__;
|
||||||
export const enableLazyPublicInstanceInFabric = __VARIANT__;
|
export const enableLazyPublicInstanceInFabric = __VARIANT__;
|
||||||
export const renameElementSymbol = __VARIANT__;
|
export const renameElementSymbol = __VARIANT__;
|
||||||
|
export const enableFragmentRefs = __VARIANT__;
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export const {
|
||||||
enableFastAddPropertiesInDiffing,
|
enableFastAddPropertiesInDiffing,
|
||||||
enableLazyPublicInstanceInFabric,
|
enableLazyPublicInstanceInFabric,
|
||||||
renameElementSymbol,
|
renameElementSymbol,
|
||||||
|
enableFragmentRefs,
|
||||||
} = dynamicFlags;
|
} = dynamicFlags;
|
||||||
|
|
||||||
// The rest of the flags are static for better dead code elimination.
|
// 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 enableScrollEndPolyfill = true;
|
||||||
export const enableSuspenseyImages = false;
|
export const enableSuspenseyImages = false;
|
||||||
export const enableSrcObject = false;
|
export const enableSrcObject = false;
|
||||||
export const enableFragmentRefs = false;
|
|
||||||
export const ownerStackLimit = 1e4;
|
export const ownerStackLimit = 1e4;
|
||||||
|
|
||||||
// Flow magic to verify the exports of this file match the original version.
|
// Flow magic to verify the exports of this file match the original version.
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
"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.",
|
"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.",
|
"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…
Reference in New Issue
Block a user