mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
Add fragment handles to children of FragmentInstances (#34935)
This PR adds a `unstable_reactFragments?: Set<FragmentInstance>` property to DOM nodes that belong to a Fragment with a ref (top level host components). This allows you to access a FragmentInstance from a DOM node. This is flagged behind `enableFragmentRefsInstanceHandles`. The primary use case to unblock is reusing IntersectionObserver instances. A fairly common practice is to cache and reuse IntersectionObservers that share the same config, with a map of node->callbacks to run for each entry in the IO callback. Currently this is not possible with Fragment Ref `observeUsing` because the key in the cache would have to be the `FragmentInstance` and you can't find it without a handle from the node. This works now by accessing `entry.target.fragments`. This also opens up possibilities to use `FragmentInstance` operations in other places, such as events. We can do `event.target.unstable_reactFragments`, then access `fragmentInstance.getClientRects` for example. In a future PR, we can assign an event's `currentTarget` as the Fragment Ref for a more direct handle when the event has been dispatched by the Fragment itself. The first commit here implemented a handle only on observed elements. This is awkward because there isn't a good way to document or expose this temporary property. `element.fragments` is closer to what we would expect from a DOM API if a standard was implemented here. And by assigning it to all top-level nodes of a Fragment, it can be used beyond the cached IntersectionObserver callback. One tradeoff here is adding extra work during the creation of FragmentInstances as well as keeping track of adding/removing nodes. Previously we only track the Fiber on creation but here we add a traversal which could apply to a large set of top-level host children. The `element.unstable_reactFragments` Set can also be randomly ordered.
This commit is contained in:
@@ -126,6 +126,7 @@ import {
|
||||
enableHydrationChangeEvent,
|
||||
enableFragmentRefsScrollIntoView,
|
||||
enableProfilerTimer,
|
||||
enableFragmentRefsInstanceHandles,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
HostComponent,
|
||||
@@ -214,6 +215,10 @@ export type Container =
|
||||
export type Instance = Element;
|
||||
export type TextInstance = Text;
|
||||
|
||||
type InstanceWithFragmentHandles = Instance & {
|
||||
unstable_reactFragments?: Set<FragmentInstanceType>,
|
||||
};
|
||||
|
||||
declare class ActivityInterface extends Comment {}
|
||||
declare class SuspenseInterface extends Comment {
|
||||
_reactRetry: void | (() => void);
|
||||
@@ -3390,10 +3395,44 @@ if (enableFragmentRefsScrollIntoView) {
|
||||
};
|
||||
}
|
||||
|
||||
function addFragmentHandleToFiber(
|
||||
child: Fiber,
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
): boolean {
|
||||
if (enableFragmentRefsInstanceHandles) {
|
||||
const instance =
|
||||
getInstanceFromHostFiber<InstanceWithFragmentHandles>(child);
|
||||
if (instance != null) {
|
||||
addFragmentHandleToInstance(instance, fragmentInstance);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function addFragmentHandleToInstance(
|
||||
instance: InstanceWithFragmentHandles,
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
): void {
|
||||
if (enableFragmentRefsInstanceHandles) {
|
||||
if (instance.unstable_reactFragments == null) {
|
||||
instance.unstable_reactFragments = new Set();
|
||||
}
|
||||
instance.unstable_reactFragments.add(fragmentInstance);
|
||||
}
|
||||
}
|
||||
|
||||
export function createFragmentInstance(
|
||||
fragmentFiber: Fiber,
|
||||
): FragmentInstanceType {
|
||||
return new (FragmentInstance: any)(fragmentFiber);
|
||||
const fragmentInstance = new (FragmentInstance: any)(fragmentFiber);
|
||||
if (enableFragmentRefsInstanceHandles) {
|
||||
traverseFragmentInstance(
|
||||
fragmentFiber,
|
||||
addFragmentHandleToFiber,
|
||||
fragmentInstance,
|
||||
);
|
||||
}
|
||||
return fragmentInstance;
|
||||
}
|
||||
|
||||
export function updateFragmentInstanceFiber(
|
||||
@@ -3404,7 +3443,7 @@ export function updateFragmentInstanceFiber(
|
||||
}
|
||||
|
||||
export function commitNewChildToFragmentInstance(
|
||||
childInstance: Instance,
|
||||
childInstance: InstanceWithFragmentHandles,
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
): void {
|
||||
const eventListeners = fragmentInstance._eventListeners;
|
||||
@@ -3419,17 +3458,25 @@ export function commitNewChildToFragmentInstance(
|
||||
observer.observe(childInstance);
|
||||
});
|
||||
}
|
||||
if (enableFragmentRefsInstanceHandles) {
|
||||
addFragmentHandleToInstance(childInstance, fragmentInstance);
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteChildFromFragmentInstance(
|
||||
childElement: Instance,
|
||||
childInstance: InstanceWithFragmentHandles,
|
||||
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);
|
||||
childInstance.removeEventListener(type, listener, optionsOrUseCapture);
|
||||
}
|
||||
}
|
||||
if (enableFragmentRefsInstanceHandles) {
|
||||
if (childInstance.unstable_reactFragments != null) {
|
||||
childInstance.unstable_reactFragments.delete(fragmentInstance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,53 @@ describe('FragmentRefs', () => {
|
||||
await act(() => root.render(<Test />));
|
||||
});
|
||||
|
||||
// @gate enableFragmentRefs && enableFragmentRefsInstanceHandles
|
||||
it('attaches fragment handles to nodes', async () => {
|
||||
const fragmentParentRef = React.createRef();
|
||||
const fragmentRef = React.createRef();
|
||||
|
||||
function Test({show}) {
|
||||
return (
|
||||
<Fragment ref={fragmentParentRef}>
|
||||
<Fragment ref={fragmentRef}>
|
||||
<div id="childA">A</div>
|
||||
<div id="childB">B</div>
|
||||
</Fragment>
|
||||
<div id="childC">C</div>
|
||||
{show && <div id="childD">D</div>}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(() => root.render(<Test show={false} />));
|
||||
|
||||
const childA = document.querySelector('#childA');
|
||||
const childB = document.querySelector('#childB');
|
||||
const childC = document.querySelector('#childC');
|
||||
|
||||
expect(childA.unstable_reactFragments.has(fragmentRef.current)).toBe(true);
|
||||
expect(childB.unstable_reactFragments.has(fragmentRef.current)).toBe(true);
|
||||
expect(childC.unstable_reactFragments.has(fragmentRef.current)).toBe(false);
|
||||
expect(childA.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(childB.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(childC.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
await act(() => root.render(<Test show={true} />));
|
||||
|
||||
const childD = document.querySelector('#childD');
|
||||
expect(childD.unstable_reactFragments.has(fragmentRef.current)).toBe(false);
|
||||
expect(childD.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
describe('focus methods', () => {
|
||||
describe('focus()', () => {
|
||||
// @gate enableFragmentRefs
|
||||
@@ -1045,6 +1092,126 @@ describe('FragmentRefs', () => {
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableFragmentRefs && enableFragmentRefsInstanceHandles
|
||||
it('attaches handles to observed elements to allow caching of observers', async () => {
|
||||
const targetToCallbackMap = new WeakMap();
|
||||
let cachedObserver = null;
|
||||
function createObserverIfNeeded(fragmentInstance, onIntersection) {
|
||||
const callbacks = targetToCallbackMap.get(fragmentInstance);
|
||||
targetToCallbackMap.set(
|
||||
fragmentInstance,
|
||||
callbacks ? [...callbacks, onIntersection] : [onIntersection],
|
||||
);
|
||||
if (cachedObserver !== null) {
|
||||
return cachedObserver;
|
||||
}
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
const fragmentInstances = entry.target.unstable_reactFragments;
|
||||
if (fragmentInstances) {
|
||||
Array.from(fragmentInstances).forEach(fInstance => {
|
||||
const cbs = targetToCallbackMap.get(fInstance) || [];
|
||||
cbs.forEach(callback => {
|
||||
callback(entry);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
targetToCallbackMap.get(entry.target)?.forEach(callback => {
|
||||
callback(entry);
|
||||
});
|
||||
});
|
||||
});
|
||||
cachedObserver = observer;
|
||||
return observer;
|
||||
}
|
||||
|
||||
function IntersectionObserverFragment({onIntersection, children}) {
|
||||
const fragmentRef = React.useRef(null);
|
||||
React.useLayoutEffect(() => {
|
||||
const observer = createObserverIfNeeded(
|
||||
fragmentRef.current,
|
||||
onIntersection,
|
||||
);
|
||||
fragmentRef.current.observeUsing(observer);
|
||||
const lastRefValue = fragmentRef.current;
|
||||
return () => {
|
||||
lastRefValue.unobserveUsing(observer);
|
||||
};
|
||||
}, []);
|
||||
return <React.Fragment ref={fragmentRef}>{children}</React.Fragment>;
|
||||
}
|
||||
|
||||
let logs = [];
|
||||
function logIntersection(id) {
|
||||
logs.push(`observe: ${id}`);
|
||||
}
|
||||
|
||||
function ChildWithManualIO({id}) {
|
||||
const divRef = React.useRef(null);
|
||||
React.useLayoutEffect(() => {
|
||||
const observer = createObserverIfNeeded(divRef.current, entry => {
|
||||
logIntersection(id);
|
||||
});
|
||||
observer.observe(divRef.current);
|
||||
return () => {
|
||||
observer.unobserve(divRef.current);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div id={id} ref={divRef}>
|
||||
{id}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Test() {
|
||||
return (
|
||||
<>
|
||||
<IntersectionObserverFragment
|
||||
onIntersection={() => logIntersection('grandparent')}>
|
||||
<IntersectionObserverFragment
|
||||
onIntersection={() => logIntersection('parentA')}>
|
||||
<div id="childA">A</div>
|
||||
</IntersectionObserverFragment>
|
||||
</IntersectionObserverFragment>
|
||||
<IntersectionObserverFragment
|
||||
onIntersection={() => logIntersection('parentB')}>
|
||||
<div id="childB">B</div>
|
||||
<ChildWithManualIO id="childC" />
|
||||
</IntersectionObserverFragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(() => root.render(<Test />));
|
||||
|
||||
simulateIntersection([
|
||||
container.querySelector('#childA'),
|
||||
{y: 0, x: 0, width: 1, height: 1},
|
||||
1,
|
||||
]);
|
||||
expect(logs).toEqual(['observe: grandparent', 'observe: parentA']);
|
||||
|
||||
logs = [];
|
||||
|
||||
simulateIntersection([
|
||||
container.querySelector('#childB'),
|
||||
{y: 0, x: 0, width: 1, height: 1},
|
||||
1,
|
||||
]);
|
||||
expect(logs).toEqual(['observe: parentB']);
|
||||
|
||||
logs = [];
|
||||
simulateIntersection([
|
||||
container.querySelector('#childC'),
|
||||
{y: 0, x: 0, width: 1, height: 1},
|
||||
1,
|
||||
]);
|
||||
expect(logs).toEqual(['observe: parentB', 'observe: childC']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getClientRects', () => {
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
type PublicTextInstance,
|
||||
type PublicRootInstance,
|
||||
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
|
||||
import {enableFragmentRefsInstanceHandles} from 'shared/ReactFeatureFlags';
|
||||
|
||||
const {
|
||||
createNode,
|
||||
@@ -119,6 +120,9 @@ export type TextInstance = {
|
||||
};
|
||||
export type HydratableInstance = Instance | TextInstance;
|
||||
export type PublicInstance = ReactNativePublicInstance;
|
||||
type PublicInstanceWithFragmentHandles = PublicInstance & {
|
||||
unstable_reactFragments?: Set<FragmentInstanceType>,
|
||||
};
|
||||
export type Container = {
|
||||
containerTag: number,
|
||||
publicInstance: PublicRootInstance | null,
|
||||
@@ -794,10 +798,45 @@ function collectClientRects(child: Fiber, rects: Array<DOMRect>): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function addFragmentHandleToFiber(
|
||||
child: Fiber,
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
): boolean {
|
||||
if (enableFragmentRefsInstanceHandles) {
|
||||
const instance = ((getPublicInstanceFromHostFiber(
|
||||
child,
|
||||
): any): PublicInstanceWithFragmentHandles);
|
||||
if (instance != null) {
|
||||
addFragmentHandleToInstance(instance, fragmentInstance);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function addFragmentHandleToInstance(
|
||||
instance: PublicInstanceWithFragmentHandles,
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
): void {
|
||||
if (enableFragmentRefsInstanceHandles) {
|
||||
if (instance.unstable_reactFragments == null) {
|
||||
instance.unstable_reactFragments = new Set();
|
||||
}
|
||||
instance.unstable_reactFragments.add(fragmentInstance);
|
||||
}
|
||||
}
|
||||
|
||||
export function createFragmentInstance(
|
||||
fragmentFiber: Fiber,
|
||||
): FragmentInstanceType {
|
||||
return new (FragmentInstance: any)(fragmentFiber);
|
||||
const fragmentInstance = new (FragmentInstance: any)(fragmentFiber);
|
||||
if (enableFragmentRefsInstanceHandles) {
|
||||
traverseFragmentInstance(
|
||||
fragmentFiber,
|
||||
addFragmentHandleToFiber,
|
||||
fragmentInstance,
|
||||
);
|
||||
}
|
||||
return fragmentInstance;
|
||||
}
|
||||
|
||||
export function updateFragmentInstanceFiber(
|
||||
@@ -821,13 +860,26 @@ export function commitNewChildToFragmentInstance(
|
||||
observer.observe(publicInstance);
|
||||
});
|
||||
}
|
||||
if (enableFragmentRefsInstanceHandles) {
|
||||
addFragmentHandleToInstance(
|
||||
((publicInstance: any): PublicInstanceWithFragmentHandles),
|
||||
fragmentInstance,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteChildFromFragmentInstance(
|
||||
child: Instance,
|
||||
childInstance: Instance,
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
): void {
|
||||
// Noop
|
||||
const publicInstance = ((getPublicInstance(
|
||||
childInstance,
|
||||
): any): PublicInstanceWithFragmentHandles);
|
||||
if (enableFragmentRefsInstanceHandles) {
|
||||
if (publicInstance.unstable_reactFragments != null) {
|
||||
publicInstance.unstable_reactFragments.delete(fragmentInstance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const NotPendingTransition: TransitionStatus = null;
|
||||
|
||||
@@ -147,6 +147,7 @@ export const enableInfiniteRenderLoopDetection: boolean = false;
|
||||
|
||||
export const enableFragmentRefs: boolean = true;
|
||||
export const enableFragmentRefsScrollIntoView: boolean = true;
|
||||
export const enableFragmentRefsInstanceHandles: boolean = false;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Ready for next major.
|
||||
|
||||
@@ -25,4 +25,5 @@ export const passChildrenWhenCloningPersistedNodes = __VARIANT__;
|
||||
export const renameElementSymbol = __VARIANT__;
|
||||
export const enableFragmentRefs = __VARIANT__;
|
||||
export const enableFragmentRefsScrollIntoView = __VARIANT__;
|
||||
export const enableFragmentRefsInstanceHandles = __VARIANT__;
|
||||
export const enableComponentPerformanceTrack = __VARIANT__;
|
||||
|
||||
@@ -27,6 +27,7 @@ export const {
|
||||
renameElementSymbol,
|
||||
enableFragmentRefs,
|
||||
enableFragmentRefsScrollIntoView,
|
||||
enableFragmentRefsInstanceHandles,
|
||||
} = dynamicFlags;
|
||||
|
||||
// The rest of the flags are static for better dead code elimination.
|
||||
|
||||
@@ -74,6 +74,7 @@ export const ownerStackLimit = 1e4;
|
||||
|
||||
export const enableFragmentRefs: boolean = true;
|
||||
export const enableFragmentRefsScrollIntoView: boolean = false;
|
||||
export const enableFragmentRefsInstanceHandles: boolean = false;
|
||||
|
||||
// Profiling Only
|
||||
export const enableProfilerTimer: boolean = __PROFILE__;
|
||||
|
||||
@@ -75,6 +75,7 @@ export const ownerStackLimit = 1e4;
|
||||
|
||||
export const enableFragmentRefs: boolean = true;
|
||||
export const enableFragmentRefsScrollIntoView: boolean = true;
|
||||
export const enableFragmentRefsInstanceHandles: boolean = 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
|
||||
|
||||
@@ -82,6 +82,7 @@ export const enableDefaultTransitionIndicator: boolean = true;
|
||||
|
||||
export const enableFragmentRefs: boolean = false;
|
||||
export const enableFragmentRefsScrollIntoView: boolean = false;
|
||||
export const enableFragmentRefsInstanceHandles: boolean = false;
|
||||
export const ownerStackLimit = 1e4;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
|
||||
@@ -111,5 +111,7 @@ export const enableDefaultTransitionIndicator: boolean = true;
|
||||
|
||||
export const ownerStackLimit = 1e4;
|
||||
|
||||
export const enableFragmentRefsInstanceHandles: boolean = true;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||
|
||||
Reference in New Issue
Block a user