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
7 changes: 3 additions & 4 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,9 @@ function filterDebugInfo(
return;
}

// Remove any debug info entries that arrived after the defined end time.
// Remove any debug info entries after the defined end time. For async info
// that means we're including anything that was awaited before the end time,
// but it doesn't need to be resolved before the end time.
const relativeEndTime =
response._debugEndTime -
// $FlowFixMe[prop-missing]
Expand All @@ -521,9 +523,6 @@ function filterDebugInfo(
if (typeof info.time === 'number' && info.time > relativeEndTime) {
break;
}
if (info.awaited != null && info.awaited.end > relativeEndTime) {
break;
}
debugInfo.push(info);
}
value._debugInfo = debugInfo;
Expand Down
55 changes: 51 additions & 4 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ import {
enableHydrationChangeEvent,
enableFragmentRefsScrollIntoView,
enableProfilerTimer,
enableFragmentRefsInstanceHandles,
} from 'shared/ReactFeatureFlags';
import {
HostComponent,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand All @@ -3404,7 +3443,7 @@ export function updateFragmentInstanceFiber(
}

export function commitNewChildToFragmentInstance(
childInstance: Instance,
childInstance: InstanceWithFragmentHandles,
fragmentInstance: FragmentInstanceType,
): void {
const eventListeners = fragmentInstance._eventListeners;
Expand All @@ -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);
}
}
}
Expand Down
167 changes: 167 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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', () => {
Expand Down
58 changes: 55 additions & 3 deletions packages/react-native-renderer/src/ReactFiberConfigFabric.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
type PublicTextInstance,
type PublicRootInstance,
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
import {enableFragmentRefsInstanceHandles} from 'shared/ReactFeatureFlags';

const {
createNode,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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;
Expand Down
Loading
Loading