Skip to content

Commit edd05f1

Browse files
authored
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.
1 parent 67f7d47 commit edd05f1

10 files changed

Lines changed: 281 additions & 7 deletions

‎packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js‎

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ import {
126126
enableHydrationChangeEvent,
127127
enableFragmentRefsScrollIntoView,
128128
enableProfilerTimer,
129+
enableFragmentRefsInstanceHandles,
129130
}from'shared/ReactFeatureFlags';
130131
import{
131132
HostComponent,
@@ -214,6 +215,10 @@ export type Container =
214215
exporttypeInstance=Element;
215216
exporttypeTextInstance=Text;
216217

218+
typeInstanceWithFragmentHandles=Instance&{
219+
unstable_reactFragments?: Set<FragmentInstanceType>,
220+
};
221+
217222
declareclassActivityInterfaceextendsComment{}
218223
declareclassSuspenseInterfaceextendsComment{
219224
_reactRetry: void|(()=>void);
@@ -3390,10 +3395,44 @@ if (enableFragmentRefsScrollIntoView) {
33903395
};
33913396
}
33923397

3398+
functionaddFragmentHandleToFiber(
3399+
child: Fiber,
3400+
fragmentInstance: FragmentInstanceType,
3401+
): boolean{
3402+
if(enableFragmentRefsInstanceHandles){
3403+
constinstance=
3404+
getInstanceFromHostFiber<InstanceWithFragmentHandles>(child);
3405+
if(instance!=null){
3406+
addFragmentHandleToInstance(instance,fragmentInstance);
3407+
}
3408+
}
3409+
returnfalse;
3410+
}
3411+
3412+
functionaddFragmentHandleToInstance(
3413+
instance: InstanceWithFragmentHandles,
3414+
fragmentInstance: FragmentInstanceType,
3415+
): void{
3416+
if(enableFragmentRefsInstanceHandles){
3417+
if(instance.unstable_reactFragments==null){
3418+
instance.unstable_reactFragments=newSet();
3419+
}
3420+
instance.unstable_reactFragments.add(fragmentInstance);
3421+
}
3422+
}
3423+
33933424
exportfunctioncreateFragmentInstance(
33943425
fragmentFiber: Fiber,
33953426
): FragmentInstanceType{
3396-
returnnew(FragmentInstance: any)(fragmentFiber);
3427+
constfragmentInstance=new(FragmentInstance: any)(fragmentFiber);
3428+
if(enableFragmentRefsInstanceHandles){
3429+
traverseFragmentInstance(
3430+
fragmentFiber,
3431+
addFragmentHandleToFiber,
3432+
fragmentInstance,
3433+
);
3434+
}
3435+
returnfragmentInstance;
33973436
}
33983437

33993438
exportfunctionupdateFragmentInstanceFiber(
@@ -3404,7 +3443,7 @@ export function updateFragmentInstanceFiber(
34043443
}
34053444

34063445
exportfunctioncommitNewChildToFragmentInstance(
3407-
childInstance: Instance,
3446+
childInstance: InstanceWithFragmentHandles,
34083447
fragmentInstance: FragmentInstanceType,
34093448
): void{
34103449
const eventListeners =fragmentInstance._eventListeners;
@@ -3419,17 +3458,25 @@ export function commitNewChildToFragmentInstance(
34193458
observer.observe(childInstance);
34203459
});
34213460
}
3461+
if(enableFragmentRefsInstanceHandles){
3462+
addFragmentHandleToInstance(childInstance,fragmentInstance);
3463+
}
34223464
}
34233465

34243466
exportfunctiondeleteChildFromFragmentInstance(
3425-
childElement: Instance,
3467+
childInstance: InstanceWithFragmentHandles,
34263468
fragmentInstance: FragmentInstanceType,
34273469
): void{
34283470
consteventListeners=fragmentInstance._eventListeners;
34293471
if(eventListeners!==null){
34303472
for(leti=0;i<eventListeners.length;i++){
34313473
const{type,listener,optionsOrUseCapture}=eventListeners[i];
3432-
childElement.removeEventListener(type,listener,optionsOrUseCapture);
3474+
childInstance.removeEventListener(type,listener,optionsOrUseCapture);
3475+
}
3476+
}
3477+
if(enableFragmentRefsInstanceHandles){
3478+
if(childInstance.unstable_reactFragments!=null){
3479+
childInstance.unstable_reactFragments.delete(fragmentInstance);
34333480
}
34343481
}
34353482
}

‎packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js‎

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,53 @@ describe('FragmentRefs', () => {
110110
awaitact(()=>root.render(<Test/>));
111111
});
112112

113+
// @gate enableFragmentRefs && enableFragmentRefsInstanceHandles
114+
it('attaches fragment handles to nodes',async()=>{
115+
constfragmentParentRef=React.createRef();
116+
constfragmentRef=React.createRef();
117+
118+
functionTest({show}){
119+
return(
120+
<Fragmentref={fragmentParentRef}>
121+
<Fragmentref={fragmentRef}>
122+
<divid="childA">A</div>
123+
<divid="childB">B</div>
124+
</Fragment>
125+
<divid="childC">C</div>
126+
{show&&<divid="childD">D</div>}
127+
</Fragment>
128+
);
129+
}
130+
131+
constroot=ReactDOMClient.createRoot(container);
132+
awaitact(()=>root.render(<Testshow={false}/>));
133+
134+
constchildA=document.querySelector('#childA');
135+
constchildB=document.querySelector('#childB');
136+
constchildC=document.querySelector('#childC');
137+
138+
expect(childA.unstable_reactFragments.has(fragmentRef.current)).toBe(true);
139+
expect(childB.unstable_reactFragments.has(fragmentRef.current)).toBe(true);
140+
expect(childC.unstable_reactFragments.has(fragmentRef.current)).toBe(false);
141+
expect(childA.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
142+
true,
143+
);
144+
expect(childB.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
145+
true,
146+
);
147+
expect(childC.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
148+
true,
149+
);
150+
151+
awaitact(()=>root.render(<Testshow={true}/>));
152+
153+
constchildD=document.querySelector('#childD');
154+
expect(childD.unstable_reactFragments.has(fragmentRef.current)).toBe(false);
155+
expect(childD.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
156+
true,
157+
);
158+
});
159+
113160
describe('focus methods',()=>{
114161
describe('focus()',()=>{
115162
// @gate enableFragmentRefs
@@ -1045,6 +1092,126 @@ describe('FragmentRefs', () => {
10451092
{withoutStack: true},
10461093
);
10471094
});
1095+
1096+
// @gate enableFragmentRefs && enableFragmentRefsInstanceHandles
1097+
it('attaches handles to observed elements to allow caching of observers',async()=>{
1098+
consttargetToCallbackMap=newWeakMap();
1099+
letcachedObserver=null;
1100+
functioncreateObserverIfNeeded(fragmentInstance,onIntersection){
1101+
constcallbacks=targetToCallbackMap.get(fragmentInstance);
1102+
targetToCallbackMap.set(
1103+
fragmentInstance,
1104+
callbacks ? [...callbacks,onIntersection] : [onIntersection],
1105+
);
1106+
if(cachedObserver!==null){
1107+
returncachedObserver;
1108+
}
1109+
constobserver=newIntersectionObserver(entries=>{
1110+
entries.forEach(entry=>{
1111+
constfragmentInstances=entry.target.unstable_reactFragments;
1112+
if(fragmentInstances){
1113+
Array.from(fragmentInstances).forEach(fInstance=>{
1114+
constcbs=targetToCallbackMap.get(fInstance)||[];
1115+
cbs.forEach(callback=>{
1116+
callback(entry);
1117+
});
1118+
});
1119+
}
1120+
1121+
targetToCallbackMap.get(entry.target)?.forEach(callback=>{
1122+
callback(entry);
1123+
});
1124+
});
1125+
});
1126+
cachedObserver=observer;
1127+
returnobserver;
1128+
}
1129+
1130+
functionIntersectionObserverFragment({onIntersection, children}){
1131+
constfragmentRef=React.useRef(null);
1132+
React.useLayoutEffect(()=>{
1133+
constobserver=createObserverIfNeeded(
1134+
fragmentRef.current,
1135+
onIntersection,
1136+
);
1137+
fragmentRef.current.observeUsing(observer);
1138+
constlastRefValue=fragmentRef.current;
1139+
return()=>{
1140+
lastRefValue.unobserveUsing(observer);
1141+
};
1142+
},[]);
1143+
return<React.Fragmentref={fragmentRef}>{children}</React.Fragment>;
1144+
}
1145+
1146+
letlogs=[];
1147+
functionlogIntersection(id){
1148+
logs.push(`observe: ${id}`);
1149+
}
1150+
1151+
functionChildWithManualIO({id}){
1152+
constdivRef=React.useRef(null);
1153+
React.useLayoutEffect(()=>{
1154+
constobserver=createObserverIfNeeded(divRef.current,entry=>{
1155+
logIntersection(id);
1156+
});
1157+
observer.observe(divRef.current);
1158+
return()=>{
1159+
observer.unobserve(divRef.current);
1160+
};
1161+
},[]);
1162+
return(
1163+
<divid={id}ref={divRef}>
1164+
{id}
1165+
</div>
1166+
);
1167+
}
1168+
1169+
functionTest(){
1170+
return(
1171+
<>
1172+
<IntersectionObserverFragment
1173+
onIntersection={()=>logIntersection('grandparent')}>
1174+
<IntersectionObserverFragment
1175+
onIntersection={()=>logIntersection('parentA')}>
1176+
<divid="childA">A</div>
1177+
</IntersectionObserverFragment>
1178+
</IntersectionObserverFragment>
1179+
<IntersectionObserverFragment
1180+
onIntersection={()=>logIntersection('parentB')}>
1181+
<divid="childB">B</div>
1182+
<ChildWithManualIOid="childC"/>
1183+
</IntersectionObserverFragment>
1184+
</>
1185+
);
1186+
}
1187+
1188+
constroot=ReactDOMClient.createRoot(container);
1189+
awaitact(()=>root.render(<Test/>));
1190+
1191+
simulateIntersection([
1192+
container.querySelector('#childA'),
1193+
{y: 0,x: 0,width: 1,height: 1},
1194+
1,
1195+
]);
1196+
expect(logs).toEqual(['observe: grandparent','observe: parentA']);
1197+
1198+
logs=[];
1199+
1200+
simulateIntersection([
1201+
container.querySelector('#childB'),
1202+
{y: 0,x: 0,width: 1,height: 1},
1203+
1,
1204+
]);
1205+
expect(logs).toEqual(['observe: parentB']);
1206+
1207+
logs=[];
1208+
simulateIntersection([
1209+
container.querySelector('#childC'),
1210+
{y: 0,x: 0,width: 1,height: 1},
1211+
1,
1212+
]);
1213+
expect(logs).toEqual(['observe: parentB','observe: childC']);
1214+
});
10481215
});
10491216

10501217
describe('getClientRects',()=>{

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

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
typePublicTextInstance,
4141
typePublicRootInstance,
4242
}from'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
43+
import{enableFragmentRefsInstanceHandles}from'shared/ReactFeatureFlags';
4344

4445
const{
4546
createNode,
@@ -119,6 +120,9 @@ export type TextInstance = {
119120
};
120121
exporttypeHydratableInstance=Instance|TextInstance;
121122
exporttypePublicInstance=ReactNativePublicInstance;
123+
typePublicInstanceWithFragmentHandles=PublicInstance&{
124+
unstable_reactFragments?: Set<FragmentInstanceType>,
125+
};
122126
exporttypeContainer={
123127
containerTag: number,
124128
publicInstance: PublicRootInstance|null,
@@ -794,10 +798,45 @@ function collectClientRects(child: Fiber, rects: Array<DOMRect>): boolean {
794798
returnfalse;
795799
}
796800

801+
function addFragmentHandleToFiber(
802+
child: Fiber,
803+
fragmentInstance: FragmentInstanceType,
804+
): boolean {
805+
if(enableFragmentRefsInstanceHandles){
806+
constinstance=((getPublicInstanceFromHostFiber(
807+
child,
808+
): any): PublicInstanceWithFragmentHandles);
809+
if(instance!=null){
810+
addFragmentHandleToInstance(instance,fragmentInstance);
811+
}
812+
}
813+
return false;
814+
}
815+
816+
functionaddFragmentHandleToInstance(
817+
instance: PublicInstanceWithFragmentHandles,
818+
fragmentInstance: FragmentInstanceType,
819+
): void{
820+
if(enableFragmentRefsInstanceHandles){
821+
if(instance.unstable_reactFragments==null){
822+
instance.unstable_reactFragments=newSet();
823+
}
824+
instance.unstable_reactFragments.add(fragmentInstance);
825+
}
826+
}
827+
797828
exportfunctioncreateFragmentInstance(
798829
fragmentFiber: Fiber,
799830
): FragmentInstanceType{
800-
returnnew(FragmentInstance: any)(fragmentFiber);
831+
constfragmentInstance=new(FragmentInstance: any)(fragmentFiber);
832+
if(enableFragmentRefsInstanceHandles){
833+
traverseFragmentInstance(
834+
fragmentFiber,
835+
addFragmentHandleToFiber,
836+
fragmentInstance,
837+
);
838+
}
839+
returnfragmentInstance;
801840
}
802841

803842
exportfunctionupdateFragmentInstanceFiber(
@@ -821,13 +860,26 @@ export function commitNewChildToFragmentInstance(
821860
observer.observe(publicInstance);
822861
});
823862
}
863+
if(enableFragmentRefsInstanceHandles){
864+
addFragmentHandleToInstance(
865+
((publicInstance: any): PublicInstanceWithFragmentHandles),
866+
fragmentInstance,
867+
);
868+
}
824869
}
825870

826871
exportfunctiondeleteChildFromFragmentInstance(
827-
child: Instance,
872+
childInstance: Instance,
828873
fragmentInstance: FragmentInstanceType,
829874
): void{
830-
// Noop
875+
constpublicInstance=((getPublicInstance(
876+
childInstance,
877+
): any): PublicInstanceWithFragmentHandles);
878+
if(enableFragmentRefsInstanceHandles){
879+
if(publicInstance.unstable_reactFragments!=null){
880+
publicInstance.unstable_reactFragments.delete(fragmentInstance);
881+
}
882+
}
831883
}
832884

833885
export const NotPendingTransition: TransitionStatus = null;

‎packages/shared/ReactFeatureFlags.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export const enableInfiniteRenderLoopDetection: boolean = false;
147147

148148
exportconstenableFragmentRefs: boolean=true;
149149
exportconstenableFragmentRefsScrollIntoView: boolean=true;
150+
exportconstenableFragmentRefsInstanceHandles: boolean=false;
150151

151152
// -----------------------------------------------------------------------------
152153
// Ready for next major.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ export const passChildrenWhenCloningPersistedNodes = __VARIANT__;
2525
exportconstrenameElementSymbol=__VARIANT__;
2626
exportconstenableFragmentRefs=__VARIANT__;
2727
exportconstenableFragmentRefsScrollIntoView=__VARIANT__;
28+
exportconstenableFragmentRefsInstanceHandles=__VARIANT__;
2829
exportconstenableComponentPerformanceTrack=__VARIANT__;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const {
2727
renameElementSymbol,
2828
enableFragmentRefs,
2929
enableFragmentRefsScrollIntoView,
30+
enableFragmentRefsInstanceHandles,
3031
}=dynamicFlags;
3132

3233
// The rest of the flags are static for better dead code elimination.

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Add fragment handles to children of FragmentInstances (#34935) · react/react@edd05f1 · GitHub
Skip to content

Commit edd05f1

Browse files
authored
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.
1 parent 67f7d47 commit edd05f1

10 files changed

Lines changed: 281 additions & 7 deletions

‎packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js‎

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ import {
126126
enableHydrationChangeEvent,
127127
enableFragmentRefsScrollIntoView,
128128
enableProfilerTimer,
129+
enableFragmentRefsInstanceHandles,
129130
}from'shared/ReactFeatureFlags';
130131
import{
131132
HostComponent,
@@ -214,6 +215,10 @@ export type Container =
214215
exporttypeInstance=Element;
215216
exporttypeTextInstance=Text;
216217

218+
typeInstanceWithFragmentHandles=Instance&{
219+
unstable_reactFragments?: Set<FragmentInstanceType>,
220+
};
221+
217222
declareclassActivityInterfaceextendsComment{}
218223
declareclassSuspenseInterfaceextendsComment{
219224
_reactRetry: void|(()=>void);
@@ -3390,10 +3395,44 @@ if (enableFragmentRefsScrollIntoView) {
33903395
};
33913396
}
33923397

3398+
functionaddFragmentHandleToFiber(
3399+
child: Fiber,
3400+
fragmentInstance: FragmentInstanceType,
3401+
): boolean{
3402+
if(enableFragmentRefsInstanceHandles){
3403+
constinstance=
3404+
getInstanceFromHostFiber<InstanceWithFragmentHandles>(child);
3405+
if(instance!=null){
3406+
addFragmentHandleToInstance(instance,fragmentInstance);
3407+
}
3408+
}
3409+
returnfalse;
3410+
}
3411+
3412+
functionaddFragmentHandleToInstance(
3413+
instance: InstanceWithFragmentHandles,
3414+
fragmentInstance: FragmentInstanceType,
3415+
): void{
3416+
if(enableFragmentRefsInstanceHandles){
3417+
if(instance.unstable_reactFragments==null){
3418+
instance.unstable_reactFragments=newSet();
3419+
}
3420+
instance.unstable_reactFragments.add(fragmentInstance);
3421+
}
3422+
}
3423+
33933424
exportfunctioncreateFragmentInstance(
33943425
fragmentFiber: Fiber,
33953426
): FragmentInstanceType{
3396-
returnnew(FragmentInstance: any)(fragmentFiber);
3427+
constfragmentInstance=new(FragmentInstance: any)(fragmentFiber);
3428+
if(enableFragmentRefsInstanceHandles){
3429+
traverseFragmentInstance(
3430+
fragmentFiber,
3431+
addFragmentHandleToFiber,
3432+
fragmentInstance,
3433+
);
3434+
}
3435+
returnfragmentInstance;
33973436
}
33983437

33993438
exportfunctionupdateFragmentInstanceFiber(
@@ -3404,7 +3443,7 @@ export function updateFragmentInstanceFiber(
34043443
}
34053444

34063445
exportfunctioncommitNewChildToFragmentInstance(
3407-
childInstance: Instance,
3446+
childInstance: InstanceWithFragmentHandles,
34083447
fragmentInstance: FragmentInstanceType,
34093448
): void{
34103449
const eventListeners =fragmentInstance._eventListeners;
@@ -3419,17 +3458,25 @@ export function commitNewChildToFragmentInstance(
34193458
observer.observe(childInstance);
34203459
});
34213460
}
3461+
if(enableFragmentRefsInstanceHandles){
3462+
addFragmentHandleToInstance(childInstance,fragmentInstance);
3463+
}
34223464
}
34233465

34243466
exportfunctiondeleteChildFromFragmentInstance(
3425-
childElement: Instance,
3467+
childInstance: InstanceWithFragmentHandles,
34263468
fragmentInstance: FragmentInstanceType,
34273469
): void{
34283470
consteventListeners=fragmentInstance._eventListeners;
34293471
if(eventListeners!==null){
34303472
for(leti=0;i<eventListeners.length;i++){
34313473
const{type,listener,optionsOrUseCapture}=eventListeners[i];
3432-
childElement.removeEventListener(type,listener,optionsOrUseCapture);
3474+
childInstance.removeEventListener(type,listener,optionsOrUseCapture);
3475+
}
3476+
}
3477+
if(enableFragmentRefsInstanceHandles){
3478+
if(childInstance.unstable_reactFragments!=null){
3479+
childInstance.unstable_reactFragments.delete(fragmentInstance);
34333480
}
34343481
}
34353482
}

‎packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js‎

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,53 @@ describe('FragmentRefs', () => {
110110
awaitact(()=>root.render(<Test/>));
111111
});
112112

113+
// @gate enableFragmentRefs && enableFragmentRefsInstanceHandles
114+
it('attaches fragment handles to nodes',async()=>{
115+
constfragmentParentRef=React.createRef();
116+
constfragmentRef=React.createRef();
117+
118+
functionTest({show}){
119+
return(
120+
<Fragmentref={fragmentParentRef}>
121+
<Fragmentref={fragmentRef}>
122+
<divid="childA">A</div>
123+
<divid="childB">B</div>
124+
</Fragment>
125+
<divid="childC">C</div>
126+
{show&&<divid="childD">D</div>}
127+
</Fragment>
128+
);
129+
}
130+
131+
constroot=ReactDOMClient.createRoot(container);
132+
awaitact(()=>root.render(<Testshow={false}/>));
133+
134+
constchildA=document.querySelector('#childA');
135+
constchildB=document.querySelector('#childB');
136+
constchildC=document.querySelector('#childC');
137+
138+
expect(childA.unstable_reactFragments.has(fragmentRef.current)).toBe(true);
139+
expect(childB.unstable_reactFragments.has(fragmentRef.current)).toBe(true);
140+
expect(childC.unstable_reactFragments.has(fragmentRef.current)).toBe(false);
141+
expect(childA.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
142+
true,
143+
);
144+
expect(childB.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
145+
true,
146+
);
147+
expect(childC.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
148+
true,
149+
);
150+
151+
awaitact(()=>root.render(<Testshow={true}/>));
152+
153+
constchildD=document.querySelector('#childD');
154+
expect(childD.unstable_reactFragments.has(fragmentRef.current)).toBe(false);
155+
expect(childD.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
156+
true,
157+
);
158+
});
159+
113160
describe('focus methods',()=>{
114161
describe('focus()',()=>{
115162
// @gate enableFragmentRefs
@@ -1045,6 +1092,126 @@ describe('FragmentRefs', () => {
10451092
{withoutStack: true},
10461093
);
10471094
});
1095+
1096+
// @gate enableFragmentRefs && enableFragmentRefsInstanceHandles
1097+
it('attaches handles to observed elements to allow caching of observers',async()=>{
1098+
consttargetToCallbackMap=newWeakMap();
1099+
letcachedObserver=null;
1100+
functioncreateObserverIfNeeded(fragmentInstance,onIntersection){
1101+
constcallbacks=targetToCallbackMap.get(fragmentInstance);
1102+
targetToCallbackMap.set(
1103+
fragmentInstance,
1104+
callbacks ? [...callbacks,onIntersection] : [onIntersection],
1105+
);
1106+
if(cachedObserver!==null){
1107+
returncachedObserver;
1108+
}
1109+
constobserver=newIntersectionObserver(entries=>{
1110+
entries.forEach(entry=>{
1111+
constfragmentInstances=entry.target.unstable_reactFragments;
1112+
if(fragmentInstances){
1113+
Array.from(fragmentInstances).forEach(fInstance=>{
1114+
constcbs=targetToCallbackMap.get(fInstance)||[];
1115+
cbs.forEach(callback=>{
1116+
callback(entry);
1117+
});
1118+
});
1119+
}
1120+
1121+
targetToCallbackMap.get(entry.target)?.forEach(callback=>{
1122+
callback(entry);
1123+
});
1124+
});
1125+
});
1126+
cachedObserver=observer;
1127+
returnobserver;
1128+
}
1129+
1130+
functionIntersectionObserverFragment({onIntersection, children}){
1131+
constfragmentRef=React.useRef(null);
1132+
React.useLayoutEffect(()=>{
1133+
constobserver=createObserverIfNeeded(
1134+
fragmentRef.current,
1135+
onIntersection,
1136+
);
1137+
fragmentRef.current.observeUsing(observer);
1138+
constlastRefValue=fragmentRef.current;
1139+
return()=>{
1140+
lastRefValue.unobserveUsing(observer);
1141+
};
1142+
},[]);
1143+
return<React.Fragmentref={fragmentRef}>{children}</React.Fragment>;
1144+
}
1145+
1146+
letlogs=[];
1147+
functionlogIntersection(id){
1148+
logs.push(`observe: ${id}`);
1149+
}
1150+
1151+
functionChildWithManualIO({id}){
1152+
constdivRef=React.useRef(null);
1153+
React.useLayoutEffect(()=>{
1154+
constobserver=createObserverIfNeeded(divRef.current,entry=>{
1155+
logIntersection(id);
1156+
});
1157+
observer.observe(divRef.current);
1158+
return()=>{
1159+
observer.unobserve(divRef.current);
1160+
};
1161+
},[]);
1162+
return(
1163+
<divid={id}ref={divRef}>
1164+
{id}
1165+
</div>
1166+
);
1167+
}
1168+
1169+
functionTest(){
1170+
return(
1171+
<>
1172+
<IntersectionObserverFragment
1173+
onIntersection={()=>logIntersection('grandparent')}>
1174+
<IntersectionObserverFragment
1175+
onIntersection={()=>logIntersection('parentA')}>
1176+
<divid="childA">A</div>
1177+
</IntersectionObserverFragment>
1178+
</IntersectionObserverFragment>
1179+
<IntersectionObserverFragment
1180+
onIntersection={()=>logIntersection('parentB')}>
1181+
<divid="childB">B</div>
1182+
<ChildWithManualIOid="childC"/>
1183+
</IntersectionObserverFragment>
1184+
</>
1185+
);
1186+
}
1187+
1188+
constroot=ReactDOMClient.createRoot(container);
1189+
awaitact(()=>root.render(<Test/>));
1190+
1191+
simulateIntersection([
1192+
container.querySelector('#childA'),
1193+
{y: 0,x: 0,width: 1,height: 1},
1194+
1,
1195+
]);
1196+
expect(logs).toEqual(['observe: grandparent','observe: parentA']);
1197+
1198+
logs=[];
1199+
1200+
simulateIntersection([
1201+
container.querySelector('#childB'),
1202+
{y: 0,x: 0,width: 1,height: 1},
1203+
1,
1204+
]);
1205+
expect(logs).toEqual(['observe: parentB']);
1206+
1207+
logs=[];
1208+
simulateIntersection([
1209+
container.querySelector('#childC'),
1210+
{y: 0,x: 0,width: 1,height: 1},
1211+
1,
1212+
]);
1213+
expect(logs).toEqual(['observe: parentB','observe: childC']);
1214+
});
10481215
});
10491216

10501217
describe('getClientRects',()=>{

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

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
typePublicTextInstance,
4141
typePublicRootInstance,
4242
}from'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
43+
import{enableFragmentRefsInstanceHandles}from'shared/ReactFeatureFlags';
4344

4445
const{
4546
createNode,
@@ -119,6 +120,9 @@ export type TextInstance = {
119120
};
120121
exporttypeHydratableInstance=Instance|TextInstance;
121122
exporttypePublicInstance=ReactNativePublicInstance;
123+
typePublicInstanceWithFragmentHandles=PublicInstance&{
124+
unstable_reactFragments?: Set<FragmentInstanceType>,
125+
};
122126
exporttypeContainer={
123127
containerTag: number,
124128
publicInstance: PublicRootInstance|null,
@@ -794,10 +798,45 @@ function collectClientRects(child: Fiber, rects: Array<DOMRect>): boolean {
794798
returnfalse;
795799
}
796800

801+
function addFragmentHandleToFiber(
802+
child: Fiber,
803+
fragmentInstance: FragmentInstanceType,
804+
): boolean {
805+
if(enableFragmentRefsInstanceHandles){
806+
constinstance=((getPublicInstanceFromHostFiber(
807+
child,
808+
): any): PublicInstanceWithFragmentHandles);
809+
if(instance!=null){
810+
addFragmentHandleToInstance(instance,fragmentInstance);
811+
}
812+
}
813+
return false;
814+
}
815+
816+
functionaddFragmentHandleToInstance(
817+
instance: PublicInstanceWithFragmentHandles,
818+
fragmentInstance: FragmentInstanceType,
819+
): void{
820+
if(enableFragmentRefsInstanceHandles){
821+
if(instance.unstable_reactFragments==null){
822+
instance.unstable_reactFragments=newSet();
823+
}
824+
instance.unstable_reactFragments.add(fragmentInstance);
825+
}
826+
}
827+
797828
exportfunctioncreateFragmentInstance(
798829
fragmentFiber: Fiber,
799830
): FragmentInstanceType{
800-
returnnew(FragmentInstance: any)(fragmentFiber);
831+
constfragmentInstance=new(FragmentInstance: any)(fragmentFiber);
832+
if(enableFragmentRefsInstanceHandles){
833+
traverseFragmentInstance(
834+
fragmentFiber,
835+
addFragmentHandleToFiber,
836+
fragmentInstance,
837+
);
838+
}
839+
returnfragmentInstance;
801840
}
802841

803842
exportfunctionupdateFragmentInstanceFiber(
@@ -821,13 +860,26 @@ export function commitNewChildToFragmentInstance(
821860
observer.observe(publicInstance);
822861
});
823862
}
863+
if(enableFragmentRefsInstanceHandles){
864+
addFragmentHandleToInstance(
865+
((publicInstance: any): PublicInstanceWithFragmentHandles),
866+
fragmentInstance,
867+
);
868+
}
824869
}
825870

826871
exportfunctiondeleteChildFromFragmentInstance(
827-
child: Instance,
872+
childInstance: Instance,
828873
fragmentInstance: FragmentInstanceType,
829874
): void{
830-
// Noop
875+
constpublicInstance=((getPublicInstance(
876+
childInstance,
877+
): any): PublicInstanceWithFragmentHandles);
878+
if(enableFragmentRefsInstanceHandles){
879+
if(publicInstance.unstable_reactFragments!=null){
880+
publicInstance.unstable_reactFragments.delete(fragmentInstance);
881+
}
882+
}
831883
}
832884

833885
export const NotPendingTransition: TransitionStatus = null;

‎packages/shared/ReactFeatureFlags.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export const enableInfiniteRenderLoopDetection: boolean = false;
147147

148148
exportconstenableFragmentRefs: boolean=true;
149149
exportconstenableFragmentRefsScrollIntoView: boolean=true;
150+
exportconstenableFragmentRefsInstanceHandles: boolean=false;
150151

151152
// -----------------------------------------------------------------------------
152153
// Ready for next major.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ export const passChildrenWhenCloningPersistedNodes = __VARIANT__;
2525
exportconstrenameElementSymbol=__VARIANT__;
2626
exportconstenableFragmentRefs=__VARIANT__;
2727
exportconstenableFragmentRefsScrollIntoView=__VARIANT__;
28+
exportconstenableFragmentRefsInstanceHandles=__VARIANT__;
2829
exportconstenableComponentPerformanceTrack=__VARIANT__;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const {
2727
renameElementSymbol,
2828
enableFragmentRefs,
2929
enableFragmentRefsScrollIntoView,
30+
enableFragmentRefsInstanceHandles,
3031
}=dynamicFlags;
3132

3233
// The rest of the flags are static for better dead code elimination.

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add fragment handles to children of FragmentInstances (#34935) · react/react@edd05f1 · GitHub
Skip to content

Commit edd05f1

Browse files
authored
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.
1 parent 67f7d47 commit edd05f1

10 files changed

Lines changed: 281 additions & 7 deletions

‎packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js‎

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ import {
126126
enableHydrationChangeEvent,
127127
enableFragmentRefsScrollIntoView,
128128
enableProfilerTimer,
129+
enableFragmentRefsInstanceHandles,
129130
}from'shared/ReactFeatureFlags';
130131
import{
131132
HostComponent,
@@ -214,6 +215,10 @@ export type Container =
214215
exporttypeInstance=Element;
215216
exporttypeTextInstance=Text;
216217

218+
typeInstanceWithFragmentHandles=Instance&{
219+
unstable_reactFragments?: Set<FragmentInstanceType>,
220+
};
221+
217222
declareclassActivityInterfaceextendsComment{}
218223
declareclassSuspenseInterfaceextendsComment{
219224
_reactRetry: void|(()=>void);
@@ -3390,10 +3395,44 @@ if (enableFragmentRefsScrollIntoView) {
33903395
};
33913396
}
33923397

3398+
functionaddFragmentHandleToFiber(
3399+
child: Fiber,
3400+
fragmentInstance: FragmentInstanceType,
3401+
): boolean{
3402+
if(enableFragmentRefsInstanceHandles){
3403+
constinstance=
3404+
getInstanceFromHostFiber<InstanceWithFragmentHandles>(child);
3405+
if(instance!=null){
3406+
addFragmentHandleToInstance(instance,fragmentInstance);
3407+
}
3408+
}
3409+
returnfalse;
3410+
}
3411+
3412+
functionaddFragmentHandleToInstance(
3413+
instance: InstanceWithFragmentHandles,
3414+
fragmentInstance: FragmentInstanceType,
3415+
): void{
3416+
if(enableFragmentRefsInstanceHandles){
3417+
if(instance.unstable_reactFragments==null){
3418+
instance.unstable_reactFragments=newSet();
3419+
}
3420+
instance.unstable_reactFragments.add(fragmentInstance);
3421+
}
3422+
}
3423+
33933424
exportfunctioncreateFragmentInstance(
33943425
fragmentFiber: Fiber,
33953426
): FragmentInstanceType{
3396-
returnnew(FragmentInstance: any)(fragmentFiber);
3427+
constfragmentInstance=new(FragmentInstance: any)(fragmentFiber);
3428+
if(enableFragmentRefsInstanceHandles){
3429+
traverseFragmentInstance(
3430+
fragmentFiber,
3431+
addFragmentHandleToFiber,
3432+
fragmentInstance,
3433+
);
3434+
}
3435+
returnfragmentInstance;
33973436
}
33983437

33993438
exportfunctionupdateFragmentInstanceFiber(
@@ -3404,7 +3443,7 @@ export function updateFragmentInstanceFiber(
34043443
}
34053444

34063445
exportfunctioncommitNewChildToFragmentInstance(
3407-
childInstance: Instance,
3446+
childInstance: InstanceWithFragmentHandles,
34083447
fragmentInstance: FragmentInstanceType,
34093448
): void{
34103449
const eventListeners =fragmentInstance._eventListeners;
@@ -3419,17 +3458,25 @@ export function commitNewChildToFragmentInstance(
34193458
observer.observe(childInstance);
34203459
});
34213460
}
3461+
if(enableFragmentRefsInstanceHandles){
3462+
addFragmentHandleToInstance(childInstance,fragmentInstance);
3463+
}
34223464
}
34233465

34243466
exportfunctiondeleteChildFromFragmentInstance(
3425-
childElement: Instance,
3467+
childInstance: InstanceWithFragmentHandles,
34263468
fragmentInstance: FragmentInstanceType,
34273469
): void{
34283470
consteventListeners=fragmentInstance._eventListeners;
34293471
if(eventListeners!==null){
34303472
for(leti=0;i<eventListeners.length;i++){
34313473
const{type,listener,optionsOrUseCapture}=eventListeners[i];
3432-
childElement.removeEventListener(type,listener,optionsOrUseCapture);
3474+
childInstance.removeEventListener(type,listener,optionsOrUseCapture);
3475+
}
3476+
}
3477+
if(enableFragmentRefsInstanceHandles){
3478+
if(childInstance.unstable_reactFragments!=null){
3479+
childInstance.unstable_reactFragments.delete(fragmentInstance);
34333480
}
34343481
}
34353482
}

‎packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js‎

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,53 @@ describe('FragmentRefs', () => {
110110
awaitact(()=>root.render(<Test/>));
111111
});
112112

113+
// @gate enableFragmentRefs && enableFragmentRefsInstanceHandles
114+
it('attaches fragment handles to nodes',async()=>{
115+
constfragmentParentRef=React.createRef();
116+
constfragmentRef=React.createRef();
117+
118+
functionTest({show}){
119+
return(
120+
<Fragmentref={fragmentParentRef}>
121+
<Fragmentref={fragmentRef}>
122+
<divid="childA">A</div>
123+
<divid="childB">B</div>
124+
</Fragment>
125+
<divid="childC">C</div>
126+
{show&&<divid="childD">D</div>}
127+
</Fragment>
128+
);
129+
}
130+
131+
constroot=ReactDOMClient.createRoot(container);
132+
awaitact(()=>root.render(<Testshow={false}/>));
133+
134+
constchildA=document.querySelector('#childA');
135+
constchildB=document.querySelector('#childB');
136+
constchildC=document.querySelector('#childC');
137+
138+
expect(childA.unstable_reactFragments.has(fragmentRef.current)).toBe(true);
139+
expect(childB.unstable_reactFragments.has(fragmentRef.current)).toBe(true);
140+
expect(childC.unstable_reactFragments.has(fragmentRef.current)).toBe(false);
141+
expect(childA.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
142+
true,
143+
);
144+
expect(childB.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
145+
true,
146+
);
147+
expect(childC.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
148+
true,
149+
);
150+
151+
awaitact(()=>root.render(<Testshow={true}/>));
152+
153+
constchildD=document.querySelector('#childD');
154+
expect(childD.unstable_reactFragments.has(fragmentRef.current)).toBe(false);
155+
expect(childD.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
156+
true,
157+
);
158+
});
159+
113160
describe('focus methods',()=>{
114161
describe('focus()',()=>{
115162
// @gate enableFragmentRefs
@@ -1045,6 +1092,126 @@ describe('FragmentRefs', () => {
10451092
{withoutStack: true},
10461093
);
10471094
});
1095+
1096+
// @gate enableFragmentRefs && enableFragmentRefsInstanceHandles
1097+
it('attaches handles to observed elements to allow caching of observers',async()=>{
1098+
consttargetToCallbackMap=newWeakMap();
1099+
letcachedObserver=null;
1100+
functioncreateObserverIfNeeded(fragmentInstance,onIntersection){
1101+
constcallbacks=targetToCallbackMap.get(fragmentInstance);
1102+
targetToCallbackMap.set(
1103+
fragmentInstance,
1104+
callbacks ? [...callbacks,onIntersection] : [onIntersection],
1105+
);
1106+
if(cachedObserver!==null){
1107+
returncachedObserver;
1108+
}
1109+
constobserver=newIntersectionObserver(entries=>{
1110+
entries.forEach(entry=>{
1111+
constfragmentInstances=entry.target.unstable_reactFragments;
1112+
if(fragmentInstances){
1113+
Array.from(fragmentInstances).forEach(fInstance=>{
1114+
constcbs=targetToCallbackMap.get(fInstance)||[];
1115+
cbs.forEach(callback=>{
1116+
callback(entry);
1117+
});
1118+
});
1119+
}
1120+
1121+
targetToCallbackMap.get(entry.target)?.forEach(callback=>{
1122+
callback(entry);
1123+
});
1124+
});
1125+
});
1126+
cachedObserver=observer;
1127+
returnobserver;
1128+
}
1129+
1130+
functionIntersectionObserverFragment({onIntersection, children}){
1131+
constfragmentRef=React.useRef(null);
1132+
React.useLayoutEffect(()=>{
1133+
constobserver=createObserverIfNeeded(
1134+
fragmentRef.current,
1135+
onIntersection,
1136+
);
1137+
fragmentRef.current.observeUsing(observer);
1138+
constlastRefValue=fragmentRef.current;
1139+
return()=>{
1140+
lastRefValue.unobserveUsing(observer);
1141+
};
1142+
},[]);
1143+
return<React.Fragmentref={fragmentRef}>{children}</React.Fragment>;
1144+
}
1145+
1146+
letlogs=[];
1147+
functionlogIntersection(id){
1148+
logs.push(`observe: ${id}`);
1149+
}
1150+
1151+
functionChildWithManualIO({id}){
1152+
constdivRef=React.useRef(null);
1153+
React.useLayoutEffect(()=>{
1154+
constobserver=createObserverIfNeeded(divRef.current,entry=>{
1155+
logIntersection(id);
1156+
});
1157+
observer.observe(divRef.current);
1158+
return()=>{
1159+
observer.unobserve(divRef.current);
1160+
};
1161+
},[]);
1162+
return(
1163+
<divid={id}ref={divRef}>
1164+
{id}
1165+
</div>
1166+
);
1167+
}
1168+
1169+
functionTest(){
1170+
return(
1171+
<>
1172+
<IntersectionObserverFragment
1173+
onIntersection={()=>logIntersection('grandparent')}>
1174+
<IntersectionObserverFragment
1175+
onIntersection={()=>logIntersection('parentA')}>
1176+
<divid="childA">A</div>
1177+
</IntersectionObserverFragment>
1178+
</IntersectionObserverFragment>
1179+
<IntersectionObserverFragment
1180+
onIntersection={()=>logIntersection('parentB')}>
1181+
<divid="childB">B</div>
1182+
<ChildWithManualIOid="childC"/>
1183+
</IntersectionObserverFragment>
1184+
</>
1185+
);
1186+
}
1187+
1188+
constroot=ReactDOMClient.createRoot(container);
1189+
awaitact(()=>root.render(<Test/>));
1190+
1191+
simulateIntersection([
1192+
container.querySelector('#childA'),
1193+
{y: 0,x: 0,width: 1,height: 1},
1194+
1,
1195+
]);
1196+
expect(logs).toEqual(['observe: grandparent','observe: parentA']);
1197+
1198+
logs=[];
1199+
1200+
simulateIntersection([
1201+
container.querySelector('#childB'),
1202+
{y: 0,x: 0,width: 1,height: 1},
1203+
1,
1204+
]);
1205+
expect(logs).toEqual(['observe: parentB']);
1206+
1207+
logs=[];
1208+
simulateIntersection([
1209+
container.querySelector('#childC'),
1210+
{y: 0,x: 0,width: 1,height: 1},
1211+
1,
1212+
]);
1213+
expect(logs).toEqual(['observe: parentB','observe: childC']);
1214+
});
10481215
});
10491216

10501217
describe('getClientRects',()=>{

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

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
typePublicTextInstance,
4141
typePublicRootInstance,
4242
}from'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
43+
import{enableFragmentRefsInstanceHandles}from'shared/ReactFeatureFlags';
4344

4445
const{
4546
createNode,
@@ -119,6 +120,9 @@ export type TextInstance = {
119120
};
120121
exporttypeHydratableInstance=Instance|TextInstance;
121122
exporttypePublicInstance=ReactNativePublicInstance;
123+
typePublicInstanceWithFragmentHandles=PublicInstance&{
124+
unstable_reactFragments?: Set<FragmentInstanceType>,
125+
};
122126
exporttypeContainer={
123127
containerTag: number,
124128
publicInstance: PublicRootInstance|null,
@@ -794,10 +798,45 @@ function collectClientRects(child: Fiber, rects: Array<DOMRect>): boolean {
794798
returnfalse;
795799
}
796800

801+
function addFragmentHandleToFiber(
802+
child: Fiber,
803+
fragmentInstance: FragmentInstanceType,
804+
): boolean {
805+
if(enableFragmentRefsInstanceHandles){
806+
constinstance=((getPublicInstanceFromHostFiber(
807+
child,
808+
): any): PublicInstanceWithFragmentHandles);
809+
if(instance!=null){
810+
addFragmentHandleToInstance(instance,fragmentInstance);
811+
}
812+
}
813+
return false;
814+
}
815+
816+
functionaddFragmentHandleToInstance(
817+
instance: PublicInstanceWithFragmentHandles,
818+
fragmentInstance: FragmentInstanceType,
819+
): void{
820+
if(enableFragmentRefsInstanceHandles){
821+
if(instance.unstable_reactFragments==null){
822+
instance.unstable_reactFragments=newSet();
823+
}
824+
instance.unstable_reactFragments.add(fragmentInstance);
825+
}
826+
}
827+
797828
exportfunctioncreateFragmentInstance(
798829
fragmentFiber: Fiber,
799830
): FragmentInstanceType{
800-
returnnew(FragmentInstance: any)(fragmentFiber);
831+
constfragmentInstance=new(FragmentInstance: any)(fragmentFiber);
832+
if(enableFragmentRefsInstanceHandles){
833+
traverseFragmentInstance(
834+
fragmentFiber,
835+
addFragmentHandleToFiber,
836+
fragmentInstance,
837+
);
838+
}
839+
returnfragmentInstance;
801840
}
802841

803842
exportfunctionupdateFragmentInstanceFiber(
@@ -821,13 +860,26 @@ export function commitNewChildToFragmentInstance(
821860
observer.observe(publicInstance);
822861
});
823862
}
863+
if(enableFragmentRefsInstanceHandles){
864+
addFragmentHandleToInstance(
865+
((publicInstance: any): PublicInstanceWithFragmentHandles),
866+
fragmentInstance,
867+
);
868+
}
824869
}
825870

826871
exportfunctiondeleteChildFromFragmentInstance(
827-
child: Instance,
872+
childInstance: Instance,
828873
fragmentInstance: FragmentInstanceType,
829874
): void{
830-
// Noop
875+
constpublicInstance=((getPublicInstance(
876+
childInstance,
877+
): any): PublicInstanceWithFragmentHandles);
878+
if(enableFragmentRefsInstanceHandles){
879+
if(publicInstance.unstable_reactFragments!=null){
880+
publicInstance.unstable_reactFragments.delete(fragmentInstance);
881+
}
882+
}
831883
}
832884

833885
export const NotPendingTransition: TransitionStatus = null;

‎packages/shared/ReactFeatureFlags.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export const enableInfiniteRenderLoopDetection: boolean = false;
147147

148148
exportconstenableFragmentRefs: boolean=true;
149149
exportconstenableFragmentRefsScrollIntoView: boolean=true;
150+
exportconstenableFragmentRefsInstanceHandles: boolean=false;
150151

151152
// -----------------------------------------------------------------------------
152153
// Ready for next major.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ export const passChildrenWhenCloningPersistedNodes = __VARIANT__;
2525
exportconstrenameElementSymbol=__VARIANT__;
2626
exportconstenableFragmentRefs=__VARIANT__;
2727
exportconstenableFragmentRefsScrollIntoView=__VARIANT__;
28+
exportconstenableFragmentRefsInstanceHandles=__VARIANT__;
2829
exportconstenableComponentPerformanceTrack=__VARIANT__;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const {
2727
renameElementSymbol,
2828
enableFragmentRefs,
2929
enableFragmentRefsScrollIntoView,
30+
enableFragmentRefsInstanceHandles,
3031
}=dynamicFlags;
3132

3233
// The rest of the flags are static for better dead code elimination.

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add fragment handles to children of FragmentInstances (#34935) · react/react@edd05f1 · GitHub
Skip to content

Commit edd05f1

Browse files
authored
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.
1 parent 67f7d47 commit edd05f1

10 files changed

Lines changed: 281 additions & 7 deletions

‎packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js‎

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ import {
126126
enableHydrationChangeEvent,
127127
enableFragmentRefsScrollIntoView,
128128
enableProfilerTimer,
129+
enableFragmentRefsInstanceHandles,
129130
}from'shared/ReactFeatureFlags';
130131
import{
131132
HostComponent,
@@ -214,6 +215,10 @@ export type Container =
214215
exporttypeInstance=Element;
215216
exporttypeTextInstance=Text;
216217

218+
typeInstanceWithFragmentHandles=Instance&{
219+
unstable_reactFragments?: Set<FragmentInstanceType>,
220+
};
221+
217222
declareclassActivityInterfaceextendsComment{}
218223
declareclassSuspenseInterfaceextendsComment{
219224
_reactRetry: void|(()=>void);
@@ -3390,10 +3395,44 @@ if (enableFragmentRefsScrollIntoView) {
33903395
};
33913396
}
33923397

3398+
functionaddFragmentHandleToFiber(
3399+
child: Fiber,
3400+
fragmentInstance: FragmentInstanceType,
3401+
): boolean{
3402+
if(enableFragmentRefsInstanceHandles){
3403+
constinstance=
3404+
getInstanceFromHostFiber<InstanceWithFragmentHandles>(child);
3405+
if(instance!=null){
3406+
addFragmentHandleToInstance(instance,fragmentInstance);
3407+
}
3408+
}
3409+
returnfalse;
3410+
}
3411+
3412+
functionaddFragmentHandleToInstance(
3413+
instance: InstanceWithFragmentHandles,
3414+
fragmentInstance: FragmentInstanceType,
3415+
): void{
3416+
if(enableFragmentRefsInstanceHandles){
3417+
if(instance.unstable_reactFragments==null){
3418+
instance.unstable_reactFragments=newSet();
3419+
}
3420+
instance.unstable_reactFragments.add(fragmentInstance);
3421+
}
3422+
}
3423+
33933424
exportfunctioncreateFragmentInstance(
33943425
fragmentFiber: Fiber,
33953426
): FragmentInstanceType{
3396-
returnnew(FragmentInstance: any)(fragmentFiber);
3427+
constfragmentInstance=new(FragmentInstance: any)(fragmentFiber);
3428+
if(enableFragmentRefsInstanceHandles){
3429+
traverseFragmentInstance(
3430+
fragmentFiber,
3431+
addFragmentHandleToFiber,
3432+
fragmentInstance,
3433+
);
3434+
}
3435+
returnfragmentInstance;
33973436
}
33983437

33993438
exportfunctionupdateFragmentInstanceFiber(
@@ -3404,7 +3443,7 @@ export function updateFragmentInstanceFiber(
34043443
}
34053444

34063445
exportfunctioncommitNewChildToFragmentInstance(
3407-
childInstance: Instance,
3446+
childInstance: InstanceWithFragmentHandles,
34083447
fragmentInstance: FragmentInstanceType,
34093448
): void{
34103449
const eventListeners =fragmentInstance._eventListeners;
@@ -3419,17 +3458,25 @@ export function commitNewChildToFragmentInstance(
34193458
observer.observe(childInstance);
34203459
});
34213460
}
3461+
if(enableFragmentRefsInstanceHandles){
3462+
addFragmentHandleToInstance(childInstance,fragmentInstance);
3463+
}
34223464
}
34233465

34243466
exportfunctiondeleteChildFromFragmentInstance(
3425-
childElement: Instance,
3467+
childInstance: InstanceWithFragmentHandles,
34263468
fragmentInstance: FragmentInstanceType,
34273469
): void{
34283470
consteventListeners=fragmentInstance._eventListeners;
34293471
if(eventListeners!==null){
34303472
for(leti=0;i<eventListeners.length;i++){
34313473
const{type,listener,optionsOrUseCapture}=eventListeners[i];
3432-
childElement.removeEventListener(type,listener,optionsOrUseCapture);
3474+
childInstance.removeEventListener(type,listener,optionsOrUseCapture);
3475+
}
3476+
}
3477+
if(enableFragmentRefsInstanceHandles){
3478+
if(childInstance.unstable_reactFragments!=null){
3479+
childInstance.unstable_reactFragments.delete(fragmentInstance);
34333480
}
34343481
}
34353482
}

‎packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js‎

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,53 @@ describe('FragmentRefs', () => {
110110
awaitact(()=>root.render(<Test/>));
111111
});
112112

113+
// @gate enableFragmentRefs && enableFragmentRefsInstanceHandles
114+
it('attaches fragment handles to nodes',async()=>{
115+
constfragmentParentRef=React.createRef();
116+
constfragmentRef=React.createRef();
117+
118+
functionTest({show}){
119+
return(
120+
<Fragmentref={fragmentParentRef}>
121+
<Fragmentref={fragmentRef}>
122+
<divid="childA">A</div>
123+
<divid="childB">B</div>
124+
</Fragment>
125+
<divid="childC">C</div>
126+
{show&&<divid="childD">D</div>}
127+
</Fragment>
128+
);
129+
}
130+
131+
constroot=ReactDOMClient.createRoot(container);
132+
awaitact(()=>root.render(<Testshow={false}/>));
133+
134+
constchildA=document.querySelector('#childA');
135+
constchildB=document.querySelector('#childB');
136+
constchildC=document.querySelector('#childC');
137+
138+
expect(childA.unstable_reactFragments.has(fragmentRef.current)).toBe(true);
139+
expect(childB.unstable_reactFragments.has(fragmentRef.current)).toBe(true);
140+
expect(childC.unstable_reactFragments.has(fragmentRef.current)).toBe(false);
141+
expect(childA.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
142+
true,
143+
);
144+
expect(childB.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
145+
true,
146+
);
147+
expect(childC.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
148+
true,
149+
);
150+
151+
awaitact(()=>root.render(<Testshow={true}/>));
152+
153+
constchildD=document.querySelector('#childD');
154+
expect(childD.unstable_reactFragments.has(fragmentRef.current)).toBe(false);
155+
expect(childD.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
156+
true,
157+
);
158+
});
159+
113160
describe('focus methods',()=>{
114161
describe('focus()',()=>{
115162
// @gate enableFragmentRefs
@@ -1045,6 +1092,126 @@ describe('FragmentRefs', () => {
10451092
{withoutStack: true},
10461093
);
10471094
});
1095+
1096+
// @gate enableFragmentRefs && enableFragmentRefsInstanceHandles
1097+
it('attaches handles to observed elements to allow caching of observers',async()=>{
1098+
consttargetToCallbackMap=newWeakMap();
1099+
letcachedObserver=null;
1100+
functioncreateObserverIfNeeded(fragmentInstance,onIntersection){
1101+
constcallbacks=targetToCallbackMap.get(fragmentInstance);
1102+
targetToCallbackMap.set(
1103+
fragmentInstance,
1104+
callbacks ? [...callbacks,onIntersection] : [onIntersection],
1105+
);
1106+
if(cachedObserver!==null){
1107+
returncachedObserver;
1108+
}
1109+
constobserver=newIntersectionObserver(entries=>{
1110+
entries.forEach(entry=>{
1111+
constfragmentInstances=entry.target.unstable_reactFragments;
1112+
if(fragmentInstances){
1113+
Array.from(fragmentInstances).forEach(fInstance=>{
1114+
constcbs=targetToCallbackMap.get(fInstance)||[];
1115+
cbs.forEach(callback=>{
1116+
callback(entry);
1117+
});
1118+
});
1119+
}
1120+
1121+
targetToCallbackMap.get(entry.target)?.forEach(callback=>{
1122+
callback(entry);
1123+
});
1124+
});
1125+
});
1126+
cachedObserver=observer;
1127+
returnobserver;
1128+
}
1129+
1130+
functionIntersectionObserverFragment({onIntersection, children}){
1131+
constfragmentRef=React.useRef(null);
1132+
React.useLayoutEffect(()=>{
1133+
constobserver=createObserverIfNeeded(
1134+
fragmentRef.current,
1135+
onIntersection,
1136+
);
1137+
fragmentRef.current.observeUsing(observer);
1138+
constlastRefValue=fragmentRef.current;
1139+
return()=>{
1140+
lastRefValue.unobserveUsing(observer);
1141+
};
1142+
},[]);
1143+
return<React.Fragmentref={fragmentRef}>{children}</React.Fragment>;
1144+
}
1145+
1146+
letlogs=[];
1147+
functionlogIntersection(id){
1148+
logs.push(`observe: ${id}`);
1149+
}
1150+
1151+
functionChildWithManualIO({id}){
1152+
constdivRef=React.useRef(null);
1153+
React.useLayoutEffect(()=>{
1154+
constobserver=createObserverIfNeeded(divRef.current,entry=>{
1155+
logIntersection(id);
1156+
});
1157+
observer.observe(divRef.current);
1158+
return()=>{
1159+
observer.unobserve(divRef.current);
1160+
};
1161+
},[]);
1162+
return(
1163+
<divid={id}ref={divRef}>
1164+
{id}
1165+
</div>
1166+
);
1167+
}
1168+
1169+
functionTest(){
1170+
return(
1171+
<>
1172+
<IntersectionObserverFragment
1173+
onIntersection={()=>logIntersection('grandparent')}>
1174+
<IntersectionObserverFragment
1175+
onIntersection={()=>logIntersection('parentA')}>
1176+
<divid="childA">A</div>
1177+
</IntersectionObserverFragment>
1178+
</IntersectionObserverFragment>
1179+
<IntersectionObserverFragment
1180+
onIntersection={()=>logIntersection('parentB')}>
1181+
<divid="childB">B</div>
1182+
<ChildWithManualIOid="childC"/>
1183+
</IntersectionObserverFragment>
1184+
</>
1185+
);
1186+
}
1187+
1188+
constroot=ReactDOMClient.createRoot(container);
1189+
awaitact(()=>root.render(<Test/>));
1190+
1191+
simulateIntersection([
1192+
container.querySelector('#childA'),
1193+
{y: 0,x: 0,width: 1,height: 1},
1194+
1,
1195+
]);
1196+
expect(logs).toEqual(['observe: grandparent','observe: parentA']);
1197+
1198+
logs=[];
1199+
1200+
simulateIntersection([
1201+
container.querySelector('#childB'),
1202+
{y: 0,x: 0,width: 1,height: 1},
1203+
1,
1204+
]);
1205+
expect(logs).toEqual(['observe: parentB']);
1206+
1207+
logs=[];
1208+
simulateIntersection([
1209+
container.querySelector('#childC'),
1210+
{y: 0,x: 0,width: 1,height: 1},
1211+
1,
1212+
]);
1213+
expect(logs).toEqual(['observe: parentB','observe: childC']);
1214+
});
10481215
});
10491216

10501217
describe('getClientRects',()=>{

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

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
typePublicTextInstance,
4141
typePublicRootInstance,
4242
}from'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
43+
import{enableFragmentRefsInstanceHandles}from'shared/ReactFeatureFlags';
4344

4445
const{
4546
createNode,
@@ -119,6 +120,9 @@ export type TextInstance = {
119120
};
120121
exporttypeHydratableInstance=Instance|TextInstance;
121122
exporttypePublicInstance=ReactNativePublicInstance;
123+
typePublicInstanceWithFragmentHandles=PublicInstance&{
124+
unstable_reactFragments?: Set<FragmentInstanceType>,
125+
};
122126
exporttypeContainer={
123127
containerTag: number,
124128
publicInstance: PublicRootInstance|null,
@@ -794,10 +798,45 @@ function collectClientRects(child: Fiber, rects: Array<DOMRect>): boolean {
794798
returnfalse;
795799
}
796800

801+
function addFragmentHandleToFiber(
802+
child: Fiber,
803+
fragmentInstance: FragmentInstanceType,
804+
): boolean {
805+
if(enableFragmentRefsInstanceHandles){
806+
constinstance=((getPublicInstanceFromHostFiber(
807+
child,
808+
): any): PublicInstanceWithFragmentHandles);
809+
if(instance!=null){
810+
addFragmentHandleToInstance(instance,fragmentInstance);
811+
}
812+
}
813+
return false;
814+
}
815+
816+
functionaddFragmentHandleToInstance(
817+
instance: PublicInstanceWithFragmentHandles,
818+
fragmentInstance: FragmentInstanceType,
819+
): void{
820+
if(enableFragmentRefsInstanceHandles){
821+
if(instance.unstable_reactFragments==null){
822+
instance.unstable_reactFragments=newSet();
823+
}
824+
instance.unstable_reactFragments.add(fragmentInstance);
825+
}
826+
}
827+
797828
exportfunctioncreateFragmentInstance(
798829
fragmentFiber: Fiber,
799830
): FragmentInstanceType{
800-
returnnew(FragmentInstance: any)(fragmentFiber);
831+
constfragmentInstance=new(FragmentInstance: any)(fragmentFiber);
832+
if(enableFragmentRefsInstanceHandles){
833+
traverseFragmentInstance(
834+
fragmentFiber,
835+
addFragmentHandleToFiber,
836+
fragmentInstance,
837+
);
838+
}
839+
returnfragmentInstance;
801840
}
802841

803842
exportfunctionupdateFragmentInstanceFiber(
@@ -821,13 +860,26 @@ export function commitNewChildToFragmentInstance(
821860
observer.observe(publicInstance);
822861
});
823862
}
863+
if(enableFragmentRefsInstanceHandles){
864+
addFragmentHandleToInstance(
865+
((publicInstance: any): PublicInstanceWithFragmentHandles),
866+
fragmentInstance,
867+
);
868+
}
824869
}
825870

826871
exportfunctiondeleteChildFromFragmentInstance(
827-
child: Instance,
872+
childInstance: Instance,
828873
fragmentInstance: FragmentInstanceType,
829874
): void{
830-
// Noop
875+
constpublicInstance=((getPublicInstance(
876+
childInstance,
877+
): any): PublicInstanceWithFragmentHandles);
878+
if(enableFragmentRefsInstanceHandles){
879+
if(publicInstance.unstable_reactFragments!=null){
880+
publicInstance.unstable_reactFragments.delete(fragmentInstance);
881+
}
882+
}
831883
}
832884

833885
export const NotPendingTransition: TransitionStatus = null;

‎packages/shared/ReactFeatureFlags.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export const enableInfiniteRenderLoopDetection: boolean = false;
147147

148148
exportconstenableFragmentRefs: boolean=true;
149149
exportconstenableFragmentRefsScrollIntoView: boolean=true;
150+
exportconstenableFragmentRefsInstanceHandles: boolean=false;
150151

151152
// -----------------------------------------------------------------------------
152153
// Ready for next major.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ export const passChildrenWhenCloningPersistedNodes = __VARIANT__;
2525
exportconstrenameElementSymbol=__VARIANT__;
2626
exportconstenableFragmentRefs=__VARIANT__;
2727
exportconstenableFragmentRefsScrollIntoView=__VARIANT__;
28+
exportconstenableFragmentRefsInstanceHandles=__VARIANT__;
2829
exportconstenableComponentPerformanceTrack=__VARIANT__;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const {
2727
renameElementSymbol,
2828
enableFragmentRefs,
2929
enableFragmentRefsScrollIntoView,
30+
enableFragmentRefsInstanceHandles,
3031
}=dynamicFlags;
3132

3233
// The rest of the flags are static for better dead code elimination.

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Add fragment handles to children of FragmentInstances (#34935) · react/react@edd05f1 · GitHub
Skip to content

Commit edd05f1

Browse files
authored
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.
1 parent 67f7d47 commit edd05f1

10 files changed

Lines changed: 281 additions & 7 deletions

‎packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js‎

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ import {
126126
enableHydrationChangeEvent,
127127
enableFragmentRefsScrollIntoView,
128128
enableProfilerTimer,
129+
enableFragmentRefsInstanceHandles,
129130
}from'shared/ReactFeatureFlags';
130131
import{
131132
HostComponent,
@@ -214,6 +215,10 @@ export type Container =
214215
exporttypeInstance=Element;
215216
exporttypeTextInstance=Text;
216217

218+
typeInstanceWithFragmentHandles=Instance&{
219+
unstable_reactFragments?: Set<FragmentInstanceType>,
220+
};
221+
217222
declareclassActivityInterfaceextendsComment{}
218223
declareclassSuspenseInterfaceextendsComment{
219224
_reactRetry: void|(()=>void);
@@ -3390,10 +3395,44 @@ if (enableFragmentRefsScrollIntoView) {
33903395
};
33913396
}
33923397

3398+
functionaddFragmentHandleToFiber(
3399+
child: Fiber,
3400+
fragmentInstance: FragmentInstanceType,
3401+
): boolean{
3402+
if(enableFragmentRefsInstanceHandles){
3403+
constinstance=
3404+
getInstanceFromHostFiber<InstanceWithFragmentHandles>(child);
3405+
if(instance!=null){
3406+
addFragmentHandleToInstance(instance,fragmentInstance);
3407+
}
3408+
}
3409+
returnfalse;
3410+
}
3411+
3412+
functionaddFragmentHandleToInstance(
3413+
instance: InstanceWithFragmentHandles,
3414+
fragmentInstance: FragmentInstanceType,
3415+
): void{
3416+
if(enableFragmentRefsInstanceHandles){
3417+
if(instance.unstable_reactFragments==null){
3418+
instance.unstable_reactFragments=newSet();
3419+
}
3420+
instance.unstable_reactFragments.add(fragmentInstance);
3421+
}
3422+
}
3423+
33933424
exportfunctioncreateFragmentInstance(
33943425
fragmentFiber: Fiber,
33953426
): FragmentInstanceType{
3396-
returnnew(FragmentInstance: any)(fragmentFiber);
3427+
constfragmentInstance=new(FragmentInstance: any)(fragmentFiber);
3428+
if(enableFragmentRefsInstanceHandles){
3429+
traverseFragmentInstance(
3430+
fragmentFiber,
3431+
addFragmentHandleToFiber,
3432+
fragmentInstance,
3433+
);
3434+
}
3435+
returnfragmentInstance;
33973436
}
33983437

33993438
exportfunctionupdateFragmentInstanceFiber(
@@ -3404,7 +3443,7 @@ export function updateFragmentInstanceFiber(
34043443
}
34053444

34063445
exportfunctioncommitNewChildToFragmentInstance(
3407-
childInstance: Instance,
3446+
childInstance: InstanceWithFragmentHandles,
34083447
fragmentInstance: FragmentInstanceType,
34093448
): void{
34103449
const eventListeners =fragmentInstance._eventListeners;
@@ -3419,17 +3458,25 @@ export function commitNewChildToFragmentInstance(
34193458
observer.observe(childInstance);
34203459
});
34213460
}
3461+
if(enableFragmentRefsInstanceHandles){
3462+
addFragmentHandleToInstance(childInstance,fragmentInstance);
3463+
}
34223464
}
34233465

34243466
exportfunctiondeleteChildFromFragmentInstance(
3425-
childElement: Instance,
3467+
childInstance: InstanceWithFragmentHandles,
34263468
fragmentInstance: FragmentInstanceType,
34273469
): void{
34283470
consteventListeners=fragmentInstance._eventListeners;
34293471
if(eventListeners!==null){
34303472
for(leti=0;i<eventListeners.length;i++){
34313473
const{type,listener,optionsOrUseCapture}=eventListeners[i];
3432-
childElement.removeEventListener(type,listener,optionsOrUseCapture);
3474+
childInstance.removeEventListener(type,listener,optionsOrUseCapture);
3475+
}
3476+
}
3477+
if(enableFragmentRefsInstanceHandles){
3478+
if(childInstance.unstable_reactFragments!=null){
3479+
childInstance.unstable_reactFragments.delete(fragmentInstance);
34333480
}
34343481
}
34353482
}

‎packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js‎

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,53 @@ describe('FragmentRefs', () => {
110110
awaitact(()=>root.render(<Test/>));
111111
});
112112

113+
// @gate enableFragmentRefs && enableFragmentRefsInstanceHandles
114+
it('attaches fragment handles to nodes',async()=>{
115+
constfragmentParentRef=React.createRef();
116+
constfragmentRef=React.createRef();
117+
118+
functionTest({show}){
119+
return(
120+
<Fragmentref={fragmentParentRef}>
121+
<Fragmentref={fragmentRef}>
122+
<divid="childA">A</div>
123+
<divid="childB">B</div>
124+
</Fragment>
125+
<divid="childC">C</div>
126+
{show&&<divid="childD">D</div>}
127+
</Fragment>
128+
);
129+
}
130+
131+
constroot=ReactDOMClient.createRoot(container);
132+
awaitact(()=>root.render(<Testshow={false}/>));
133+
134+
constchildA=document.querySelector('#childA');
135+
constchildB=document.querySelector('#childB');
136+
constchildC=document.querySelector('#childC');
137+
138+
expect(childA.unstable_reactFragments.has(fragmentRef.current)).toBe(true);
139+
expect(childB.unstable_reactFragments.has(fragmentRef.current)).toBe(true);
140+
expect(childC.unstable_reactFragments.has(fragmentRef.current)).toBe(false);
141+
expect(childA.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
142+
true,
143+
);
144+
expect(childB.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
145+
true,
146+
);
147+
expect(childC.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
148+
true,
149+
);
150+
151+
awaitact(()=>root.render(<Testshow={true}/>));
152+
153+
constchildD=document.querySelector('#childD');
154+
expect(childD.unstable_reactFragments.has(fragmentRef.current)).toBe(false);
155+
expect(childD.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
156+
true,
157+
);
158+
});
159+
113160
describe('focus methods',()=>{
114161
describe('focus()',()=>{
115162
// @gate enableFragmentRefs
@@ -1045,6 +1092,126 @@ describe('FragmentRefs', () => {
10451092
{withoutStack: true},
10461093
);
10471094
});
1095+
1096+
// @gate enableFragmentRefs && enableFragmentRefsInstanceHandles
1097+
it('attaches handles to observed elements to allow caching of observers',async()=>{
1098+
consttargetToCallbackMap=newWeakMap();
1099+
letcachedObserver=null;
1100+
functioncreateObserverIfNeeded(fragmentInstance,onIntersection){
1101+
constcallbacks=targetToCallbackMap.get(fragmentInstance);
1102+
targetToCallbackMap.set(
1103+
fragmentInstance,
1104+
callbacks ? [...callbacks,onIntersection] : [onIntersection],
1105+
);
1106+
if(cachedObserver!==null){
1107+
returncachedObserver;
1108+
}
1109+
constobserver=newIntersectionObserver(entries=>{
1110+
entries.forEach(entry=>{
1111+
constfragmentInstances=entry.target.unstable_reactFragments;
1112+
if(fragmentInstances){
1113+
Array.from(fragmentInstances).forEach(fInstance=>{
1114+
constcbs=targetToCallbackMap.get(fInstance)||[];
1115+
cbs.forEach(callback=>{
1116+
callback(entry);
1117+
});
1118+
});
1119+
}
1120+
1121+
targetToCallbackMap.get(entry.target)?.forEach(callback=>{
1122+
callback(entry);
1123+
});
1124+
});
1125+
});
1126+
cachedObserver=observer;
1127+
returnobserver;
1128+
}
1129+
1130+
functionIntersectionObserverFragment({onIntersection, children}){
1131+
constfragmentRef=React.useRef(null);
1132+
React.useLayoutEffect(()=>{
1133+
constobserver=createObserverIfNeeded(
1134+
fragmentRef.current,
1135+
onIntersection,
1136+
);
1137+
fragmentRef.current.observeUsing(observer);
1138+
constlastRefValue=fragmentRef.current;
1139+
return()=>{
1140+
lastRefValue.unobserveUsing(observer);
1141+
};
1142+
},[]);
1143+
return<React.Fragmentref={fragmentRef}>{children}</React.Fragment>;
1144+
}
1145+
1146+
letlogs=[];
1147+
functionlogIntersection(id){
1148+
logs.push(`observe: ${id}`);
1149+
}
1150+
1151+
functionChildWithManualIO({id}){
1152+
constdivRef=React.useRef(null);
1153+
React.useLayoutEffect(()=>{
1154+
constobserver=createObserverIfNeeded(divRef.current,entry=>{
1155+
logIntersection(id);
1156+
});
1157+
observer.observe(divRef.current);
1158+
return()=>{
1159+
observer.unobserve(divRef.current);
1160+
};
1161+
},[]);
1162+
return(
1163+
<divid={id}ref={divRef}>
1164+
{id}
1165+
</div>
1166+
);
1167+
}
1168+
1169+
functionTest(){
1170+
return(
1171+
<>
1172+
<IntersectionObserverFragment
1173+
onIntersection={()=>logIntersection('grandparent')}>
1174+
<IntersectionObserverFragment
1175+
onIntersection={()=>logIntersection('parentA')}>
1176+
<divid="childA">A</div>
1177+
</IntersectionObserverFragment>
1178+
</IntersectionObserverFragment>
1179+
<IntersectionObserverFragment
1180+
onIntersection={()=>logIntersection('parentB')}>
1181+
<divid="childB">B</div>
1182+
<ChildWithManualIOid="childC"/>
1183+
</IntersectionObserverFragment>
1184+
</>
1185+
);
1186+
}
1187+
1188+
constroot=ReactDOMClient.createRoot(container);
1189+
awaitact(()=>root.render(<Test/>));
1190+
1191+
simulateIntersection([
1192+
container.querySelector('#childA'),
1193+
{y: 0,x: 0,width: 1,height: 1},
1194+
1,
1195+
]);
1196+
expect(logs).toEqual(['observe: grandparent','observe: parentA']);
1197+
1198+
logs=[];
1199+
1200+
simulateIntersection([
1201+
container.querySelector('#childB'),
1202+
{y: 0,x: 0,width: 1,height: 1},
1203+
1,
1204+
]);
1205+
expect(logs).toEqual(['observe: parentB']);
1206+
1207+
logs=[];
1208+
simulateIntersection([
1209+
container.querySelector('#childC'),
1210+
{y: 0,x: 0,width: 1,height: 1},
1211+
1,
1212+
]);
1213+
expect(logs).toEqual(['observe: parentB','observe: childC']);
1214+
});
10481215
});
10491216

10501217
describe('getClientRects',()=>{

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

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
typePublicTextInstance,
4141
typePublicRootInstance,
4242
}from'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
43+
import{enableFragmentRefsInstanceHandles}from'shared/ReactFeatureFlags';
4344

4445
const{
4546
createNode,
@@ -119,6 +120,9 @@ export type TextInstance = {
119120
};
120121
exporttypeHydratableInstance=Instance|TextInstance;
121122
exporttypePublicInstance=ReactNativePublicInstance;
123+
typePublicInstanceWithFragmentHandles=PublicInstance&{
124+
unstable_reactFragments?: Set<FragmentInstanceType>,
125+
};
122126
exporttypeContainer={
123127
containerTag: number,
124128
publicInstance: PublicRootInstance|null,
@@ -794,10 +798,45 @@ function collectClientRects(child: Fiber, rects: Array<DOMRect>): boolean {
794798
returnfalse;
795799
}
796800

801+
function addFragmentHandleToFiber(
802+
child: Fiber,
803+
fragmentInstance: FragmentInstanceType,
804+
): boolean {
805+
if(enableFragmentRefsInstanceHandles){
806+
constinstance=((getPublicInstanceFromHostFiber(
807+
child,
808+
): any): PublicInstanceWithFragmentHandles);
809+
if(instance!=null){
810+
addFragmentHandleToInstance(instance,fragmentInstance);
811+
}
812+
}
813+
return false;
814+
}
815+
816+
functionaddFragmentHandleToInstance(
817+
instance: PublicInstanceWithFragmentHandles,
818+
fragmentInstance: FragmentInstanceType,
819+
): void{
820+
if(enableFragmentRefsInstanceHandles){
821+
if(instance.unstable_reactFragments==null){
822+
instance.unstable_reactFragments=newSet();
823+
}
824+
instance.unstable_reactFragments.add(fragmentInstance);
825+
}
826+
}
827+
797828
exportfunctioncreateFragmentInstance(
798829
fragmentFiber: Fiber,
799830
): FragmentInstanceType{
800-
returnnew(FragmentInstance: any)(fragmentFiber);
831+
constfragmentInstance=new(FragmentInstance: any)(fragmentFiber);
832+
if(enableFragmentRefsInstanceHandles){
833+
traverseFragmentInstance(
834+
fragmentFiber,
835+
addFragmentHandleToFiber,
836+
fragmentInstance,
837+
);
838+
}
839+
returnfragmentInstance;
801840
}
802841

803842
exportfunctionupdateFragmentInstanceFiber(
@@ -821,13 +860,26 @@ export function commitNewChildToFragmentInstance(
821860
observer.observe(publicInstance);
822861
});
823862
}
863+
if(enableFragmentRefsInstanceHandles){
864+
addFragmentHandleToInstance(
865+
((publicInstance: any): PublicInstanceWithFragmentHandles),
866+
fragmentInstance,
867+
);
868+
}
824869
}
825870

826871
exportfunctiondeleteChildFromFragmentInstance(
827-
child: Instance,
872+
childInstance: Instance,
828873
fragmentInstance: FragmentInstanceType,
829874
): void{
830-
// Noop
875+
constpublicInstance=((getPublicInstance(
876+
childInstance,
877+
): any): PublicInstanceWithFragmentHandles);
878+
if(enableFragmentRefsInstanceHandles){
879+
if(publicInstance.unstable_reactFragments!=null){
880+
publicInstance.unstable_reactFragments.delete(fragmentInstance);
881+
}
882+
}
831883
}
832884

833885
export const NotPendingTransition: TransitionStatus = null;

‎packages/shared/ReactFeatureFlags.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export const enableInfiniteRenderLoopDetection: boolean = false;
147147

148148
exportconstenableFragmentRefs: boolean=true;
149149
exportconstenableFragmentRefsScrollIntoView: boolean=true;
150+
exportconstenableFragmentRefsInstanceHandles: boolean=false;
150151

151152
// -----------------------------------------------------------------------------
152153
// Ready for next major.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ export const passChildrenWhenCloningPersistedNodes = __VARIANT__;
2525
exportconstrenameElementSymbol=__VARIANT__;
2626
exportconstenableFragmentRefs=__VARIANT__;
2727
exportconstenableFragmentRefsScrollIntoView=__VARIANT__;
28+
exportconstenableFragmentRefsInstanceHandles=__VARIANT__;
2829
exportconstenableComponentPerformanceTrack=__VARIANT__;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const {
2727
renameElementSymbol,
2828
enableFragmentRefs,
2929
enableFragmentRefsScrollIntoView,
30+
enableFragmentRefsInstanceHandles,
3031
}=dynamicFlags;
3132

3233
// The rest of the flags are static for better dead code elimination.

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add fragment handles to children of FragmentInstances (#34935) · react/react@edd05f1 · GitHub
Skip to content

Commit edd05f1

Browse files
authored
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.
1 parent 67f7d47 commit edd05f1

10 files changed

Lines changed: 281 additions & 7 deletions

‎packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js‎

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ import {
126126
enableHydrationChangeEvent,
127127
enableFragmentRefsScrollIntoView,
128128
enableProfilerTimer,
129+
enableFragmentRefsInstanceHandles,
129130
}from'shared/ReactFeatureFlags';
130131
import{
131132
HostComponent,
@@ -214,6 +215,10 @@ export type Container =
214215
exporttypeInstance=Element;
215216
exporttypeTextInstance=Text;
216217

218+
typeInstanceWithFragmentHandles=Instance&{
219+
unstable_reactFragments?: Set<FragmentInstanceType>,
220+
};
221+
217222
declareclassActivityInterfaceextendsComment{}
218223
declareclassSuspenseInterfaceextendsComment{
219224
_reactRetry: void|(()=>void);
@@ -3390,10 +3395,44 @@ if (enableFragmentRefsScrollIntoView) {
33903395
};
33913396
}
33923397

3398+
functionaddFragmentHandleToFiber(
3399+
child: Fiber,
3400+
fragmentInstance: FragmentInstanceType,
3401+
): boolean{
3402+
if(enableFragmentRefsInstanceHandles){
3403+
constinstance=
3404+
getInstanceFromHostFiber<InstanceWithFragmentHandles>(child);
3405+
if(instance!=null){
3406+
addFragmentHandleToInstance(instance,fragmentInstance);
3407+
}
3408+
}
3409+
returnfalse;
3410+
}
3411+
3412+
functionaddFragmentHandleToInstance(
3413+
instance: InstanceWithFragmentHandles,
3414+
fragmentInstance: FragmentInstanceType,
3415+
): void{
3416+
if(enableFragmentRefsInstanceHandles){
3417+
if(instance.unstable_reactFragments==null){
3418+
instance.unstable_reactFragments=newSet();
3419+
}
3420+
instance.unstable_reactFragments.add(fragmentInstance);
3421+
}
3422+
}
3423+
33933424
exportfunctioncreateFragmentInstance(
33943425
fragmentFiber: Fiber,
33953426
): FragmentInstanceType{
3396-
returnnew(FragmentInstance: any)(fragmentFiber);
3427+
constfragmentInstance=new(FragmentInstance: any)(fragmentFiber);
3428+
if(enableFragmentRefsInstanceHandles){
3429+
traverseFragmentInstance(
3430+
fragmentFiber,
3431+
addFragmentHandleToFiber,
3432+
fragmentInstance,
3433+
);
3434+
}
3435+
returnfragmentInstance;
33973436
}
33983437

33993438
exportfunctionupdateFragmentInstanceFiber(
@@ -3404,7 +3443,7 @@ export function updateFragmentInstanceFiber(
34043443
}
34053444

34063445
exportfunctioncommitNewChildToFragmentInstance(
3407-
childInstance: Instance,
3446+
childInstance: InstanceWithFragmentHandles,
34083447
fragmentInstance: FragmentInstanceType,
34093448
): void{
34103449
const eventListeners =fragmentInstance._eventListeners;
@@ -3419,17 +3458,25 @@ export function commitNewChildToFragmentInstance(
34193458
observer.observe(childInstance);
34203459
});
34213460
}
3461+
if(enableFragmentRefsInstanceHandles){
3462+
addFragmentHandleToInstance(childInstance,fragmentInstance);
3463+
}
34223464
}
34233465

34243466
exportfunctiondeleteChildFromFragmentInstance(
3425-
childElement: Instance,
3467+
childInstance: InstanceWithFragmentHandles,
34263468
fragmentInstance: FragmentInstanceType,
34273469
): void{
34283470
consteventListeners=fragmentInstance._eventListeners;
34293471
if(eventListeners!==null){
34303472
for(leti=0;i<eventListeners.length;i++){
34313473
const{type,listener,optionsOrUseCapture}=eventListeners[i];
3432-
childElement.removeEventListener(type,listener,optionsOrUseCapture);
3474+
childInstance.removeEventListener(type,listener,optionsOrUseCapture);
3475+
}
3476+
}
3477+
if(enableFragmentRefsInstanceHandles){
3478+
if(childInstance.unstable_reactFragments!=null){
3479+
childInstance.unstable_reactFragments.delete(fragmentInstance);
34333480
}
34343481
}
34353482
}

‎packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js‎

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,53 @@ describe('FragmentRefs', () => {
110110
awaitact(()=>root.render(<Test/>));
111111
});
112112

113+
// @gate enableFragmentRefs && enableFragmentRefsInstanceHandles
114+
it('attaches fragment handles to nodes',async()=>{
115+
constfragmentParentRef=React.createRef();
116+
constfragmentRef=React.createRef();
117+
118+
functionTest({show}){
119+
return(
120+
<Fragmentref={fragmentParentRef}>
121+
<Fragmentref={fragmentRef}>
122+
<divid="childA">A</div>
123+
<divid="childB">B</div>
124+
</Fragment>
125+
<divid="childC">C</div>
126+
{show&&<divid="childD">D</div>}
127+
</Fragment>
128+
);
129+
}
130+
131+
constroot=ReactDOMClient.createRoot(container);
132+
awaitact(()=>root.render(<Testshow={false}/>));
133+
134+
constchildA=document.querySelector('#childA');
135+
constchildB=document.querySelector('#childB');
136+
constchildC=document.querySelector('#childC');
137+
138+
expect(childA.unstable_reactFragments.has(fragmentRef.current)).toBe(true);
139+
expect(childB.unstable_reactFragments.has(fragmentRef.current)).toBe(true);
140+
expect(childC.unstable_reactFragments.has(fragmentRef.current)).toBe(false);
141+
expect(childA.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
142+
true,
143+
);
144+
expect(childB.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
145+
true,
146+
);
147+
expect(childC.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
148+
true,
149+
);
150+
151+
awaitact(()=>root.render(<Testshow={true}/>));
152+
153+
constchildD=document.querySelector('#childD');
154+
expect(childD.unstable_reactFragments.has(fragmentRef.current)).toBe(false);
155+
expect(childD.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
156+
true,
157+
);
158+
});
159+
113160
describe('focus methods',()=>{
114161
describe('focus()',()=>{
115162
// @gate enableFragmentRefs
@@ -1045,6 +1092,126 @@ describe('FragmentRefs', () => {
10451092
{withoutStack: true},
10461093
);
10471094
});
1095+
1096+
// @gate enableFragmentRefs && enableFragmentRefsInstanceHandles
1097+
it('attaches handles to observed elements to allow caching of observers',async()=>{
1098+
consttargetToCallbackMap=newWeakMap();
1099+
letcachedObserver=null;
1100+
functioncreateObserverIfNeeded(fragmentInstance,onIntersection){
1101+
constcallbacks=targetToCallbackMap.get(fragmentInstance);
1102+
targetToCallbackMap.set(
1103+
fragmentInstance,
1104+
callbacks ? [...callbacks,onIntersection] : [onIntersection],
1105+
);
1106+
if(cachedObserver!==null){
1107+
returncachedObserver;
1108+
}
1109+
constobserver=newIntersectionObserver(entries=>{
1110+
entries.forEach(entry=>{
1111+
constfragmentInstances=entry.target.unstable_reactFragments;
1112+
if(fragmentInstances){
1113+
Array.from(fragmentInstances).forEach(fInstance=>{
1114+
constcbs=targetToCallbackMap.get(fInstance)||[];
1115+
cbs.forEach(callback=>{
1116+
callback(entry);
1117+
});
1118+
});
1119+
}
1120+
1121+
targetToCallbackMap.get(entry.target)?.forEach(callback=>{
1122+
callback(entry);
1123+
});
1124+
});
1125+
});
1126+
cachedObserver=observer;
1127+
returnobserver;
1128+
}
1129+
1130+
functionIntersectionObserverFragment({onIntersection, children}){
1131+
constfragmentRef=React.useRef(null);
1132+
React.useLayoutEffect(()=>{
1133+
constobserver=createObserverIfNeeded(
1134+
fragmentRef.current,
1135+
onIntersection,
1136+
);
1137+
fragmentRef.current.observeUsing(observer);
1138+
constlastRefValue=fragmentRef.current;
1139+
return()=>{
1140+
lastRefValue.unobserveUsing(observer);
1141+
};
1142+
},[]);
1143+
return<React.Fragmentref={fragmentRef}>{children}</React.Fragment>;
1144+
}
1145+
1146+
letlogs=[];
1147+
functionlogIntersection(id){
1148+
logs.push(`observe: ${id}`);
1149+
}
1150+
1151+
functionChildWithManualIO({id}){
1152+
constdivRef=React.useRef(null);
1153+
React.useLayoutEffect(()=>{
1154+
constobserver=createObserverIfNeeded(divRef.current,entry=>{
1155+
logIntersection(id);
1156+
});
1157+
observer.observe(divRef.current);
1158+
return()=>{
1159+
observer.unobserve(divRef.current);
1160+
};
1161+
},[]);
1162+
return(
1163+
<divid={id}ref={divRef}>
1164+
{id}
1165+
</div>
1166+
);
1167+
}
1168+
1169+
functionTest(){
1170+
return(
1171+
<>
1172+
<IntersectionObserverFragment
1173+
onIntersection={()=>logIntersection('grandparent')}>
1174+
<IntersectionObserverFragment
1175+
onIntersection={()=>logIntersection('parentA')}>
1176+
<divid="childA">A</div>
1177+
</IntersectionObserverFragment>
1178+
</IntersectionObserverFragment>
1179+
<IntersectionObserverFragment
1180+
onIntersection={()=>logIntersection('parentB')}>
1181+
<divid="childB">B</div>
1182+
<ChildWithManualIOid="childC"/>
1183+
</IntersectionObserverFragment>
1184+
</>
1185+
);
1186+
}
1187+
1188+
constroot=ReactDOMClient.createRoot(container);
1189+
awaitact(()=>root.render(<Test/>));
1190+
1191+
simulateIntersection([
1192+
container.querySelector('#childA'),
1193+
{y: 0,x: 0,width: 1,height: 1},
1194+
1,
1195+
]);
1196+
expect(logs).toEqual(['observe: grandparent','observe: parentA']);
1197+
1198+
logs=[];
1199+
1200+
simulateIntersection([
1201+
container.querySelector('#childB'),
1202+
{y: 0,x: 0,width: 1,height: 1},
1203+
1,
1204+
]);
1205+
expect(logs).toEqual(['observe: parentB']);
1206+
1207+
logs=[];
1208+
simulateIntersection([
1209+
container.querySelector('#childC'),
1210+
{y: 0,x: 0,width: 1,height: 1},
1211+
1,
1212+
]);
1213+
expect(logs).toEqual(['observe: parentB','observe: childC']);
1214+
});
10481215
});
10491216

10501217
describe('getClientRects',()=>{

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

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
typePublicTextInstance,
4141
typePublicRootInstance,
4242
}from'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
43+
import{enableFragmentRefsInstanceHandles}from'shared/ReactFeatureFlags';
4344

4445
const{
4546
createNode,
@@ -119,6 +120,9 @@ export type TextInstance = {
119120
};
120121
exporttypeHydratableInstance=Instance|TextInstance;
121122
exporttypePublicInstance=ReactNativePublicInstance;
123+
typePublicInstanceWithFragmentHandles=PublicInstance&{
124+
unstable_reactFragments?: Set<FragmentInstanceType>,
125+
};
122126
exporttypeContainer={
123127
containerTag: number,
124128
publicInstance: PublicRootInstance|null,
@@ -794,10 +798,45 @@ function collectClientRects(child: Fiber, rects: Array<DOMRect>): boolean {
794798
returnfalse;
795799
}
796800

801+
function addFragmentHandleToFiber(
802+
child: Fiber,
803+
fragmentInstance: FragmentInstanceType,
804+
): boolean {
805+
if(enableFragmentRefsInstanceHandles){
806+
constinstance=((getPublicInstanceFromHostFiber(
807+
child,
808+
): any): PublicInstanceWithFragmentHandles);
809+
if(instance!=null){
810+
addFragmentHandleToInstance(instance,fragmentInstance);
811+
}
812+
}
813+
return false;
814+
}
815+
816+
functionaddFragmentHandleToInstance(
817+
instance: PublicInstanceWithFragmentHandles,
818+
fragmentInstance: FragmentInstanceType,
819+
): void{
820+
if(enableFragmentRefsInstanceHandles){
821+
if(instance.unstable_reactFragments==null){
822+
instance.unstable_reactFragments=newSet();
823+
}
824+
instance.unstable_reactFragments.add(fragmentInstance);
825+
}
826+
}
827+
797828
exportfunctioncreateFragmentInstance(
798829
fragmentFiber: Fiber,
799830
): FragmentInstanceType{
800-
returnnew(FragmentInstance: any)(fragmentFiber);
831+
constfragmentInstance=new(FragmentInstance: any)(fragmentFiber);
832+
if(enableFragmentRefsInstanceHandles){
833+
traverseFragmentInstance(
834+
fragmentFiber,
835+
addFragmentHandleToFiber,
836+
fragmentInstance,
837+
);
838+
}
839+
returnfragmentInstance;
801840
}
802841

803842
exportfunctionupdateFragmentInstanceFiber(
@@ -821,13 +860,26 @@ export function commitNewChildToFragmentInstance(
821860
observer.observe(publicInstance);
822861
});
823862
}
863+
if(enableFragmentRefsInstanceHandles){
864+
addFragmentHandleToInstance(
865+
((publicInstance: any): PublicInstanceWithFragmentHandles),
866+
fragmentInstance,
867+
);
868+
}
824869
}
825870

826871
exportfunctiondeleteChildFromFragmentInstance(
827-
child: Instance,
872+
childInstance: Instance,
828873
fragmentInstance: FragmentInstanceType,
829874
): void{
830-
// Noop
875+
constpublicInstance=((getPublicInstance(
876+
childInstance,
877+
): any): PublicInstanceWithFragmentHandles);
878+
if(enableFragmentRefsInstanceHandles){
879+
if(publicInstance.unstable_reactFragments!=null){
880+
publicInstance.unstable_reactFragments.delete(fragmentInstance);
881+
}
882+
}
831883
}
832884

833885
export const NotPendingTransition: TransitionStatus = null;

‎packages/shared/ReactFeatureFlags.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export const enableInfiniteRenderLoopDetection: boolean = false;
147147

148148
exportconstenableFragmentRefs: boolean=true;
149149
exportconstenableFragmentRefsScrollIntoView: boolean=true;
150+
exportconstenableFragmentRefsInstanceHandles: boolean=false;
150151

151152
// -----------------------------------------------------------------------------
152153
// Ready for next major.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ export const passChildrenWhenCloningPersistedNodes = __VARIANT__;
2525
exportconstrenameElementSymbol=__VARIANT__;
2626
exportconstenableFragmentRefs=__VARIANT__;
2727
exportconstenableFragmentRefsScrollIntoView=__VARIANT__;
28+
exportconstenableFragmentRefsInstanceHandles=__VARIANT__;
2829
exportconstenableComponentPerformanceTrack=__VARIANT__;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const {
2727
renameElementSymbol,
2828
enableFragmentRefs,
2929
enableFragmentRefsScrollIntoView,
30+
enableFragmentRefsInstanceHandles,
3031
}=dynamicFlags;
3132

3233
// The rest of the flags are static for better dead code elimination.

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add fragment handles to children of FragmentInstances (#34935) · react/react@edd05f1 · GitHub
Skip to content

Commit edd05f1

Browse files
authored
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.
1 parent 67f7d47 commit edd05f1

10 files changed

Lines changed: 281 additions & 7 deletions

‎packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js‎

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ import {
126126
enableHydrationChangeEvent,
127127
enableFragmentRefsScrollIntoView,
128128
enableProfilerTimer,
129+
enableFragmentRefsInstanceHandles,
129130
}from'shared/ReactFeatureFlags';
130131
import{
131132
HostComponent,
@@ -214,6 +215,10 @@ export type Container =
214215
exporttypeInstance=Element;
215216
exporttypeTextInstance=Text;
216217

218+
typeInstanceWithFragmentHandles=Instance&{
219+
unstable_reactFragments?: Set<FragmentInstanceType>,
220+
};
221+
217222
declareclassActivityInterfaceextendsComment{}
218223
declareclassSuspenseInterfaceextendsComment{
219224
_reactRetry: void|(()=>void);
@@ -3390,10 +3395,44 @@ if (enableFragmentRefsScrollIntoView) {
33903395
};
33913396
}
33923397

3398+
functionaddFragmentHandleToFiber(
3399+
child: Fiber,
3400+
fragmentInstance: FragmentInstanceType,
3401+
): boolean{
3402+
if(enableFragmentRefsInstanceHandles){
3403+
constinstance=
3404+
getInstanceFromHostFiber<InstanceWithFragmentHandles>(child);
3405+
if(instance!=null){
3406+
addFragmentHandleToInstance(instance,fragmentInstance);
3407+
}
3408+
}
3409+
returnfalse;
3410+
}
3411+
3412+
functionaddFragmentHandleToInstance(
3413+
instance: InstanceWithFragmentHandles,
3414+
fragmentInstance: FragmentInstanceType,
3415+
): void{
3416+
if(enableFragmentRefsInstanceHandles){
3417+
if(instance.unstable_reactFragments==null){
3418+
instance.unstable_reactFragments=newSet();
3419+
}
3420+
instance.unstable_reactFragments.add(fragmentInstance);
3421+
}
3422+
}
3423+
33933424
exportfunctioncreateFragmentInstance(
33943425
fragmentFiber: Fiber,
33953426
): FragmentInstanceType{
3396-
returnnew(FragmentInstance: any)(fragmentFiber);
3427+
constfragmentInstance=new(FragmentInstance: any)(fragmentFiber);
3428+
if(enableFragmentRefsInstanceHandles){
3429+
traverseFragmentInstance(
3430+
fragmentFiber,
3431+
addFragmentHandleToFiber,
3432+
fragmentInstance,
3433+
);
3434+
}
3435+
returnfragmentInstance;
33973436
}
33983437

33993438
exportfunctionupdateFragmentInstanceFiber(
@@ -3404,7 +3443,7 @@ export function updateFragmentInstanceFiber(
34043443
}
34053444

34063445
exportfunctioncommitNewChildToFragmentInstance(
3407-
childInstance: Instance,
3446+
childInstance: InstanceWithFragmentHandles,
34083447
fragmentInstance: FragmentInstanceType,
34093448
): void{
34103449
const eventListeners =fragmentInstance._eventListeners;
@@ -3419,17 +3458,25 @@ export function commitNewChildToFragmentInstance(
34193458
observer.observe(childInstance);
34203459
});
34213460
}
3461+
if(enableFragmentRefsInstanceHandles){
3462+
addFragmentHandleToInstance(childInstance,fragmentInstance);
3463+
}
34223464
}
34233465

34243466
exportfunctiondeleteChildFromFragmentInstance(
3425-
childElement: Instance,
3467+
childInstance: InstanceWithFragmentHandles,
34263468
fragmentInstance: FragmentInstanceType,
34273469
): void{
34283470
consteventListeners=fragmentInstance._eventListeners;
34293471
if(eventListeners!==null){
34303472
for(leti=0;i<eventListeners.length;i++){
34313473
const{type,listener,optionsOrUseCapture}=eventListeners[i];
3432-
childElement.removeEventListener(type,listener,optionsOrUseCapture);
3474+
childInstance.removeEventListener(type,listener,optionsOrUseCapture);
3475+
}
3476+
}
3477+
if(enableFragmentRefsInstanceHandles){
3478+
if(childInstance.unstable_reactFragments!=null){
3479+
childInstance.unstable_reactFragments.delete(fragmentInstance);
34333480
}
34343481
}
34353482
}

‎packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js‎

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,53 @@ describe('FragmentRefs', () => {
110110
awaitact(()=>root.render(<Test/>));
111111
});
112112

113+
// @gate enableFragmentRefs && enableFragmentRefsInstanceHandles
114+
it('attaches fragment handles to nodes',async()=>{
115+
constfragmentParentRef=React.createRef();
116+
constfragmentRef=React.createRef();
117+
118+
functionTest({show}){
119+
return(
120+
<Fragmentref={fragmentParentRef}>
121+
<Fragmentref={fragmentRef}>
122+
<divid="childA">A</div>
123+
<divid="childB">B</div>
124+
</Fragment>
125+
<divid="childC">C</div>
126+
{show&&<divid="childD">D</div>}
127+
</Fragment>
128+
);
129+
}
130+
131+
constroot=ReactDOMClient.createRoot(container);
132+
awaitact(()=>root.render(<Testshow={false}/>));
133+
134+
constchildA=document.querySelector('#childA');
135+
constchildB=document.querySelector('#childB');
136+
constchildC=document.querySelector('#childC');
137+
138+
expect(childA.unstable_reactFragments.has(fragmentRef.current)).toBe(true);
139+
expect(childB.unstable_reactFragments.has(fragmentRef.current)).toBe(true);
140+
expect(childC.unstable_reactFragments.has(fragmentRef.current)).toBe(false);
141+
expect(childA.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
142+
true,
143+
);
144+
expect(childB.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
145+
true,
146+
);
147+
expect(childC.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
148+
true,
149+
);
150+
151+
awaitact(()=>root.render(<Testshow={true}/>));
152+
153+
constchildD=document.querySelector('#childD');
154+
expect(childD.unstable_reactFragments.has(fragmentRef.current)).toBe(false);
155+
expect(childD.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
156+
true,
157+
);
158+
});
159+
113160
describe('focus methods',()=>{
114161
describe('focus()',()=>{
115162
// @gate enableFragmentRefs
@@ -1045,6 +1092,126 @@ describe('FragmentRefs', () => {
10451092
{withoutStack: true},
10461093
);
10471094
});
1095+
1096+
// @gate enableFragmentRefs && enableFragmentRefsInstanceHandles
1097+
it('attaches handles to observed elements to allow caching of observers',async()=>{
1098+
consttargetToCallbackMap=newWeakMap();
1099+
letcachedObserver=null;
1100+
functioncreateObserverIfNeeded(fragmentInstance,onIntersection){
1101+
constcallbacks=targetToCallbackMap.get(fragmentInstance);
1102+
targetToCallbackMap.set(
1103+
fragmentInstance,
1104+
callbacks ? [...callbacks,onIntersection] : [onIntersection],
1105+
);
1106+
if(cachedObserver!==null){
1107+
returncachedObserver;
1108+
}
1109+
constobserver=newIntersectionObserver(entries=>{
1110+
entries.forEach(entry=>{
1111+
constfragmentInstances=entry.target.unstable_reactFragments;
1112+
if(fragmentInstances){
1113+
Array.from(fragmentInstances).forEach(fInstance=>{
1114+
constcbs=targetToCallbackMap.get(fInstance)||[];
1115+
cbs.forEach(callback=>{
1116+
callback(entry);
1117+
});
1118+
});
1119+
}
1120+
1121+
targetToCallbackMap.get(entry.target)?.forEach(callback=>{
1122+
callback(entry);
1123+
});
1124+
});
1125+
});
1126+
cachedObserver=observer;
1127+
returnobserver;
1128+
}
1129+
1130+
functionIntersectionObserverFragment({onIntersection, children}){
1131+
constfragmentRef=React.useRef(null);
1132+
React.useLayoutEffect(()=>{
1133+
constobserver=createObserverIfNeeded(
1134+
fragmentRef.current,
1135+
onIntersection,
1136+
);
1137+
fragmentRef.current.observeUsing(observer);
1138+
constlastRefValue=fragmentRef.current;
1139+
return()=>{
1140+
lastRefValue.unobserveUsing(observer);
1141+
};
1142+
},[]);
1143+
return<React.Fragmentref={fragmentRef}>{children}</React.Fragment>;
1144+
}
1145+
1146+
letlogs=[];
1147+
functionlogIntersection(id){
1148+
logs.push(`observe: ${id}`);
1149+
}
1150+
1151+
functionChildWithManualIO({id}){
1152+
constdivRef=React.useRef(null);
1153+
React.useLayoutEffect(()=>{
1154+
constobserver=createObserverIfNeeded(divRef.current,entry=>{
1155+
logIntersection(id);
1156+
});
1157+
observer.observe(divRef.current);
1158+
return()=>{
1159+
observer.unobserve(divRef.current);
1160+
};
1161+
},[]);
1162+
return(
1163+
<divid={id}ref={divRef}>
1164+
{id}
1165+
</div>
1166+
);
1167+
}
1168+
1169+
functionTest(){
1170+
return(
1171+
<>
1172+
<IntersectionObserverFragment
1173+
onIntersection={()=>logIntersection('grandparent')}>
1174+
<IntersectionObserverFragment
1175+
onIntersection={()=>logIntersection('parentA')}>
1176+
<divid="childA">A</div>
1177+
</IntersectionObserverFragment>
1178+
</IntersectionObserverFragment>
1179+
<IntersectionObserverFragment
1180+
onIntersection={()=>logIntersection('parentB')}>
1181+
<divid="childB">B</div>
1182+
<ChildWithManualIOid="childC"/>
1183+
</IntersectionObserverFragment>
1184+
</>
1185+
);
1186+
}
1187+
1188+
constroot=ReactDOMClient.createRoot(container);
1189+
awaitact(()=>root.render(<Test/>));
1190+
1191+
simulateIntersection([
1192+
container.querySelector('#childA'),
1193+
{y: 0,x: 0,width: 1,height: 1},
1194+
1,
1195+
]);
1196+
expect(logs).toEqual(['observe: grandparent','observe: parentA']);
1197+
1198+
logs=[];
1199+
1200+
simulateIntersection([
1201+
container.querySelector('#childB'),
1202+
{y: 0,x: 0,width: 1,height: 1},
1203+
1,
1204+
]);
1205+
expect(logs).toEqual(['observe: parentB']);
1206+
1207+
logs=[];
1208+
simulateIntersection([
1209+
container.querySelector('#childC'),
1210+
{y: 0,x: 0,width: 1,height: 1},
1211+
1,
1212+
]);
1213+
expect(logs).toEqual(['observe: parentB','observe: childC']);
1214+
});
10481215
});
10491216

10501217
describe('getClientRects',()=>{

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

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
typePublicTextInstance,
4141
typePublicRootInstance,
4242
}from'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
43+
import{enableFragmentRefsInstanceHandles}from'shared/ReactFeatureFlags';
4344

4445
const{
4546
createNode,
@@ -119,6 +120,9 @@ export type TextInstance = {
119120
};
120121
exporttypeHydratableInstance=Instance|TextInstance;
121122
exporttypePublicInstance=ReactNativePublicInstance;
123+
typePublicInstanceWithFragmentHandles=PublicInstance&{
124+
unstable_reactFragments?: Set<FragmentInstanceType>,
125+
};
122126
exporttypeContainer={
123127
containerTag: number,
124128
publicInstance: PublicRootInstance|null,
@@ -794,10 +798,45 @@ function collectClientRects(child: Fiber, rects: Array<DOMRect>): boolean {
794798
returnfalse;
795799
}
796800

801+
function addFragmentHandleToFiber(
802+
child: Fiber,
803+
fragmentInstance: FragmentInstanceType,
804+
): boolean {
805+
if(enableFragmentRefsInstanceHandles){
806+
constinstance=((getPublicInstanceFromHostFiber(
807+
child,
808+
): any): PublicInstanceWithFragmentHandles);
809+
if(instance!=null){
810+
addFragmentHandleToInstance(instance,fragmentInstance);
811+
}
812+
}
813+
return false;
814+
}
815+
816+
functionaddFragmentHandleToInstance(
817+
instance: PublicInstanceWithFragmentHandles,
818+
fragmentInstance: FragmentInstanceType,
819+
): void{
820+
if(enableFragmentRefsInstanceHandles){
821+
if(instance.unstable_reactFragments==null){
822+
instance.unstable_reactFragments=newSet();
823+
}
824+
instance.unstable_reactFragments.add(fragmentInstance);
825+
}
826+
}
827+
797828
exportfunctioncreateFragmentInstance(
798829
fragmentFiber: Fiber,
799830
): FragmentInstanceType{
800-
returnnew(FragmentInstance: any)(fragmentFiber);
831+
constfragmentInstance=new(FragmentInstance: any)(fragmentFiber);
832+
if(enableFragmentRefsInstanceHandles){
833+
traverseFragmentInstance(
834+
fragmentFiber,
835+
addFragmentHandleToFiber,
836+
fragmentInstance,
837+
);
838+
}
839+
returnfragmentInstance;
801840
}
802841

803842
exportfunctionupdateFragmentInstanceFiber(
@@ -821,13 +860,26 @@ export function commitNewChildToFragmentInstance(
821860
observer.observe(publicInstance);
822861
});
823862
}
863+
if(enableFragmentRefsInstanceHandles){
864+
addFragmentHandleToInstance(
865+
((publicInstance: any): PublicInstanceWithFragmentHandles),
866+
fragmentInstance,
867+
);
868+
}
824869
}
825870

826871
exportfunctiondeleteChildFromFragmentInstance(
827-
child: Instance,
872+
childInstance: Instance,
828873
fragmentInstance: FragmentInstanceType,
829874
): void{
830-
// Noop
875+
constpublicInstance=((getPublicInstance(
876+
childInstance,
877+
): any): PublicInstanceWithFragmentHandles);
878+
if(enableFragmentRefsInstanceHandles){
879+
if(publicInstance.unstable_reactFragments!=null){
880+
publicInstance.unstable_reactFragments.delete(fragmentInstance);
881+
}
882+
}
831883
}
832884

833885
export const NotPendingTransition: TransitionStatus = null;

‎packages/shared/ReactFeatureFlags.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export const enableInfiniteRenderLoopDetection: boolean = false;
147147

148148
exportconstenableFragmentRefs: boolean=true;
149149
exportconstenableFragmentRefsScrollIntoView: boolean=true;
150+
exportconstenableFragmentRefsInstanceHandles: boolean=false;
150151

151152
// -----------------------------------------------------------------------------
152153
// Ready for next major.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ export const passChildrenWhenCloningPersistedNodes = __VARIANT__;
2525
exportconstrenameElementSymbol=__VARIANT__;
2626
exportconstenableFragmentRefs=__VARIANT__;
2727
exportconstenableFragmentRefsScrollIntoView=__VARIANT__;
28+
exportconstenableFragmentRefsInstanceHandles=__VARIANT__;
2829
exportconstenableComponentPerformanceTrack=__VARIANT__;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const {
2727
renameElementSymbol,
2828
enableFragmentRefs,
2929
enableFragmentRefsScrollIntoView,
30+
enableFragmentRefsInstanceHandles,
3031
}=dynamicFlags;
3132

3233
// The rest of the flags are static for better dead code elimination.

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Add fragment handles to children of FragmentInstances (#34935) · react/react@edd05f1 · GitHub
Skip to content

Commit edd05f1

Browse files
authored
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.
1 parent 67f7d47 commit edd05f1

10 files changed

Lines changed: 281 additions & 7 deletions

‎packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js‎

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ import {
126126
enableHydrationChangeEvent,
127127
enableFragmentRefsScrollIntoView,
128128
enableProfilerTimer,
129+
enableFragmentRefsInstanceHandles,
129130
}from'shared/ReactFeatureFlags';
130131
import{
131132
HostComponent,
@@ -214,6 +215,10 @@ export type Container =
214215
exporttypeInstance=Element;
215216
exporttypeTextInstance=Text;
216217

218+
typeInstanceWithFragmentHandles=Instance&{
219+
unstable_reactFragments?: Set<FragmentInstanceType>,
220+
};
221+
217222
declareclassActivityInterfaceextendsComment{}
218223
declareclassSuspenseInterfaceextendsComment{
219224
_reactRetry: void|(()=>void);
@@ -3390,10 +3395,44 @@ if (enableFragmentRefsScrollIntoView) {
33903395
};
33913396
}
33923397

3398+
functionaddFragmentHandleToFiber(
3399+
child: Fiber,
3400+
fragmentInstance: FragmentInstanceType,
3401+
): boolean{
3402+
if(enableFragmentRefsInstanceHandles){
3403+
constinstance=
3404+
getInstanceFromHostFiber<InstanceWithFragmentHandles>(child);
3405+
if(instance!=null){
3406+
addFragmentHandleToInstance(instance,fragmentInstance);
3407+
}
3408+
}
3409+
returnfalse;
3410+
}
3411+
3412+
functionaddFragmentHandleToInstance(
3413+
instance: InstanceWithFragmentHandles,
3414+
fragmentInstance: FragmentInstanceType,
3415+
): void{
3416+
if(enableFragmentRefsInstanceHandles){
3417+
if(instance.unstable_reactFragments==null){
3418+
instance.unstable_reactFragments=newSet();
3419+
}
3420+
instance.unstable_reactFragments.add(fragmentInstance);
3421+
}
3422+
}
3423+
33933424
exportfunctioncreateFragmentInstance(
33943425
fragmentFiber: Fiber,
33953426
): FragmentInstanceType{
3396-
returnnew(FragmentInstance: any)(fragmentFiber);
3427+
constfragmentInstance=new(FragmentInstance: any)(fragmentFiber);
3428+
if(enableFragmentRefsInstanceHandles){
3429+
traverseFragmentInstance(
3430+
fragmentFiber,
3431+
addFragmentHandleToFiber,
3432+
fragmentInstance,
3433+
);
3434+
}
3435+
returnfragmentInstance;
33973436
}
33983437

33993438
exportfunctionupdateFragmentInstanceFiber(
@@ -3404,7 +3443,7 @@ export function updateFragmentInstanceFiber(
34043443
}
34053444

34063445
exportfunctioncommitNewChildToFragmentInstance(
3407-
childInstance: Instance,
3446+
childInstance: InstanceWithFragmentHandles,
34083447
fragmentInstance: FragmentInstanceType,
34093448
): void{
34103449
const eventListeners =fragmentInstance._eventListeners;
@@ -3419,17 +3458,25 @@ export function commitNewChildToFragmentInstance(
34193458
observer.observe(childInstance);
34203459
});
34213460
}
3461+
if(enableFragmentRefsInstanceHandles){
3462+
addFragmentHandleToInstance(childInstance,fragmentInstance);
3463+
}
34223464
}
34233465

34243466
exportfunctiondeleteChildFromFragmentInstance(
3425-
childElement: Instance,
3467+
childInstance: InstanceWithFragmentHandles,
34263468
fragmentInstance: FragmentInstanceType,
34273469
): void{
34283470
consteventListeners=fragmentInstance._eventListeners;
34293471
if(eventListeners!==null){
34303472
for(leti=0;i<eventListeners.length;i++){
34313473
const{type,listener,optionsOrUseCapture}=eventListeners[i];
3432-
childElement.removeEventListener(type,listener,optionsOrUseCapture);
3474+
childInstance.removeEventListener(type,listener,optionsOrUseCapture);
3475+
}
3476+
}
3477+
if(enableFragmentRefsInstanceHandles){
3478+
if(childInstance.unstable_reactFragments!=null){
3479+
childInstance.unstable_reactFragments.delete(fragmentInstance);
34333480
}
34343481
}
34353482
}

‎packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js‎

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,53 @@ describe('FragmentRefs', () => {
110110
awaitact(()=>root.render(<Test/>));
111111
});
112112

113+
// @gate enableFragmentRefs && enableFragmentRefsInstanceHandles
114+
it('attaches fragment handles to nodes',async()=>{
115+
constfragmentParentRef=React.createRef();
116+
constfragmentRef=React.createRef();
117+
118+
functionTest({show}){
119+
return(
120+
<Fragmentref={fragmentParentRef}>
121+
<Fragmentref={fragmentRef}>
122+
<divid="childA">A</div>
123+
<divid="childB">B</div>
124+
</Fragment>
125+
<divid="childC">C</div>
126+
{show&&<divid="childD">D</div>}
127+
</Fragment>
128+
);
129+
}
130+
131+
constroot=ReactDOMClient.createRoot(container);
132+
awaitact(()=>root.render(<Testshow={false}/>));
133+
134+
constchildA=document.querySelector('#childA');
135+
constchildB=document.querySelector('#childB');
136+
constchildC=document.querySelector('#childC');
137+
138+
expect(childA.unstable_reactFragments.has(fragmentRef.current)).toBe(true);
139+
expect(childB.unstable_reactFragments.has(fragmentRef.current)).toBe(true);
140+
expect(childC.unstable_reactFragments.has(fragmentRef.current)).toBe(false);
141+
expect(childA.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
142+
true,
143+
);
144+
expect(childB.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
145+
true,
146+
);
147+
expect(childC.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
148+
true,
149+
);
150+
151+
awaitact(()=>root.render(<Testshow={true}/>));
152+
153+
constchildD=document.querySelector('#childD');
154+
expect(childD.unstable_reactFragments.has(fragmentRef.current)).toBe(false);
155+
expect(childD.unstable_reactFragments.has(fragmentParentRef.current)).toBe(
156+
true,
157+
);
158+
});
159+
113160
describe('focus methods',()=>{
114161
describe('focus()',()=>{
115162
// @gate enableFragmentRefs
@@ -1045,6 +1092,126 @@ describe('FragmentRefs', () => {
10451092
{withoutStack: true},
10461093
);
10471094
});
1095+
1096+
// @gate enableFragmentRefs && enableFragmentRefsInstanceHandles
1097+
it('attaches handles to observed elements to allow caching of observers',async()=>{
1098+
consttargetToCallbackMap=newWeakMap();
1099+
letcachedObserver=null;
1100+
functioncreateObserverIfNeeded(fragmentInstance,onIntersection){
1101+
constcallbacks=targetToCallbackMap.get(fragmentInstance);
1102+
targetToCallbackMap.set(
1103+
fragmentInstance,
1104+
callbacks ? [...callbacks,onIntersection] : [onIntersection],
1105+
);
1106+
if(cachedObserver!==null){
1107+
returncachedObserver;
1108+
}
1109+
constobserver=newIntersectionObserver(entries=>{
1110+
entries.forEach(entry=>{
1111+
constfragmentInstances=entry.target.unstable_reactFragments;
1112+
if(fragmentInstances){
1113+
Array.from(fragmentInstances).forEach(fInstance=>{
1114+
constcbs=targetToCallbackMap.get(fInstance)||[];
1115+
cbs.forEach(callback=>{
1116+
callback(entry);
1117+
});
1118+
});
1119+
}
1120+
1121+
targetToCallbackMap.get(entry.target)?.forEach(callback=>{
1122+
callback(entry);
1123+
});
1124+
});
1125+
});
1126+
cachedObserver=observer;
1127+
returnobserver;
1128+
}
1129+
1130+
functionIntersectionObserverFragment({onIntersection, children}){
1131+
constfragmentRef=React.useRef(null);
1132+
React.useLayoutEffect(()=>{
1133+
constobserver=createObserverIfNeeded(
1134+
fragmentRef.current,
1135+
onIntersection,
1136+
);
1137+
fragmentRef.current.observeUsing(observer);
1138+
constlastRefValue=fragmentRef.current;
1139+
return()=>{
1140+
lastRefValue.unobserveUsing(observer);
1141+
};
1142+
},[]);
1143+
return<React.Fragmentref={fragmentRef}>{children}</React.Fragment>;
1144+
}
1145+
1146+
letlogs=[];
1147+
functionlogIntersection(id){
1148+
logs.push(`observe: ${id}`);
1149+
}
1150+
1151+
functionChildWithManualIO({id}){
1152+
constdivRef=React.useRef(null);
1153+
React.useLayoutEffect(()=>{
1154+
constobserver=createObserverIfNeeded(divRef.current,entry=>{
1155+
logIntersection(id);
1156+
});
1157+
observer.observe(divRef.current);
1158+
return()=>{
1159+
observer.unobserve(divRef.current);
1160+
};
1161+
},[]);
1162+
return(
1163+
<divid={id}ref={divRef}>
1164+
{id}
1165+
</div>
1166+
);
1167+
}
1168+
1169+
functionTest(){
1170+
return(
1171+
<>
1172+
<IntersectionObserverFragment
1173+
onIntersection={()=>logIntersection('grandparent')}>
1174+
<IntersectionObserverFragment
1175+
onIntersection={()=>logIntersection('parentA')}>
1176+
<divid="childA">A</div>
1177+
</IntersectionObserverFragment>
1178+
</IntersectionObserverFragment>
1179+
<IntersectionObserverFragment
1180+
onIntersection={()=>logIntersection('parentB')}>
1181+
<divid="childB">B</div>
1182+
<ChildWithManualIOid="childC"/>
1183+
</IntersectionObserverFragment>
1184+
</>
1185+
);
1186+
}
1187+
1188+
constroot=ReactDOMClient.createRoot(container);
1189+
awaitact(()=>root.render(<Test/>));
1190+
1191+
simulateIntersection([
1192+
container.querySelector('#childA'),
1193+
{y: 0,x: 0,width: 1,height: 1},
1194+
1,
1195+
]);
1196+
expect(logs).toEqual(['observe: grandparent','observe: parentA']);
1197+
1198+
logs=[];
1199+
1200+
simulateIntersection([
1201+
container.querySelector('#childB'),
1202+
{y: 0,x: 0,width: 1,height: 1},
1203+
1,
1204+
]);
1205+
expect(logs).toEqual(['observe: parentB']);
1206+
1207+
logs=[];
1208+
simulateIntersection([
1209+
container.querySelector('#childC'),
1210+
{y: 0,x: 0,width: 1,height: 1},
1211+
1,
1212+
]);
1213+
expect(logs).toEqual(['observe: parentB','observe: childC']);
1214+
});
10481215
});
10491216

10501217
describe('getClientRects',()=>{

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

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
typePublicTextInstance,
4141
typePublicRootInstance,
4242
}from'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
43+
import{enableFragmentRefsInstanceHandles}from'shared/ReactFeatureFlags';
4344

4445
const{
4546
createNode,
@@ -119,6 +120,9 @@ export type TextInstance = {
119120
};
120121
exporttypeHydratableInstance=Instance|TextInstance;
121122
exporttypePublicInstance=ReactNativePublicInstance;
123+
typePublicInstanceWithFragmentHandles=PublicInstance&{
124+
unstable_reactFragments?: Set<FragmentInstanceType>,
125+
};
122126
exporttypeContainer={
123127
containerTag: number,
124128
publicInstance: PublicRootInstance|null,
@@ -794,10 +798,45 @@ function collectClientRects(child: Fiber, rects: Array<DOMRect>): boolean {
794798
returnfalse;
795799
}
796800

801+
function addFragmentHandleToFiber(
802+
child: Fiber,
803+
fragmentInstance: FragmentInstanceType,
804+
): boolean {
805+
if(enableFragmentRefsInstanceHandles){
806+
constinstance=((getPublicInstanceFromHostFiber(
807+
child,
808+
): any): PublicInstanceWithFragmentHandles);
809+
if(instance!=null){
810+
addFragmentHandleToInstance(instance,fragmentInstance);
811+
}
812+
}
813+
return false;
814+
}
815+
816+
functionaddFragmentHandleToInstance(
817+
instance: PublicInstanceWithFragmentHandles,
818+
fragmentInstance: FragmentInstanceType,
819+
): void{
820+
if(enableFragmentRefsInstanceHandles){
821+
if(instance.unstable_reactFragments==null){
822+
instance.unstable_reactFragments=newSet();
823+
}
824+
instance.unstable_reactFragments.add(fragmentInstance);
825+
}
826+
}
827+
797828
exportfunctioncreateFragmentInstance(
798829
fragmentFiber: Fiber,
799830
): FragmentInstanceType{
800-
returnnew(FragmentInstance: any)(fragmentFiber);
831+
constfragmentInstance=new(FragmentInstance: any)(fragmentFiber);
832+
if(enableFragmentRefsInstanceHandles){
833+
traverseFragmentInstance(
834+
fragmentFiber,
835+
addFragmentHandleToFiber,
836+
fragmentInstance,
837+
);
838+
}
839+
returnfragmentInstance;
801840
}
802841

803842
exportfunctionupdateFragmentInstanceFiber(
@@ -821,13 +860,26 @@ export function commitNewChildToFragmentInstance(
821860
observer.observe(publicInstance);
822861
});
823862
}
863+
if(enableFragmentRefsInstanceHandles){
864+
addFragmentHandleToInstance(
865+
((publicInstance: any): PublicInstanceWithFragmentHandles),
866+
fragmentInstance,
867+
);
868+
}
824869
}
825870

826871
exportfunctiondeleteChildFromFragmentInstance(
827-
child: Instance,
872+
childInstance: Instance,
828873
fragmentInstance: FragmentInstanceType,
829874
): void{
830-
// Noop
875+
constpublicInstance=((getPublicInstance(
876+
childInstance,
877+
): any): PublicInstanceWithFragmentHandles);
878+
if(enableFragmentRefsInstanceHandles){
879+
if(publicInstance.unstable_reactFragments!=null){
880+
publicInstance.unstable_reactFragments.delete(fragmentInstance);
881+
}
882+
}
831883
}
832884

833885
export const NotPendingTransition: TransitionStatus = null;

‎packages/shared/ReactFeatureFlags.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export const enableInfiniteRenderLoopDetection: boolean = false;
147147

148148
exportconstenableFragmentRefs: boolean=true;
149149
exportconstenableFragmentRefsScrollIntoView: boolean=true;
150+
exportconstenableFragmentRefsInstanceHandles: boolean=false;
150151

151152
// -----------------------------------------------------------------------------
152153
// Ready for next major.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ export const passChildrenWhenCloningPersistedNodes = __VARIANT__;
2525
exportconstrenameElementSymbol=__VARIANT__;
2626
exportconstenableFragmentRefs=__VARIANT__;
2727
exportconstenableFragmentRefsScrollIntoView=__VARIANT__;
28+
exportconstenableFragmentRefsInstanceHandles=__VARIANT__;
2829
exportconstenableComponentPerformanceTrack=__VARIANT__;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const {
2727
renameElementSymbol,
2828
enableFragmentRefs,
2929
enableFragmentRefsScrollIntoView,
30+
enableFragmentRefsInstanceHandles,
3031
}=dynamicFlags;
3132

3233
// The rest of the flags are static for better dead code elimination.

0 commit comments

Comments
 (0)