Skip to content

Commit 854bd6b

Browse files
committed
Add cached intersection observer usage example
1 parent 2bb495c commit 854bd6b

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

src/content/reference/react/Fragment.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ A bitmask of [position flags](https://developer.mozilla.org/en-US/docs/Web/API/N
248248
* `observeUsing` does not work on text nodes. React logs a warning in development if the Fragment contains only text children.
249249
* `focus`, `focusLast`, and `blur` have no effect when the Fragment contains only text children.
250250
* React does not apply event listeners added via `addEventListener` to hidden [`<Activity>`](/reference/react/Activity) trees. When an `Activity` boundary switches from hidden to visible, listeners are applied automatically.
251+
* Each first-level DOM child of a Fragment with a `ref` gets a `reactFragments` property—a `Set<FragmentInstance>` containing all Fragment instances that own the element. This enables [caching a shared observer](#caching-global-intersection-observer) across multiple Fragments.
251252

252253
---
253254

@@ -528,3 +529,78 @@ function FormFields({ children }) {
528529
```
529530
530531
`focus()` finds the first focusable element by searching depth-first through all nested children. `focusLast()` does the same in reverse. `blur()` removes focus if the currently focused element is within the Fragment.
532+
533+
---
534+
535+
### <CanaryBadge /> Caching a global IntersectionObserver {/*caching-global-intersection-observer*/}
536+
537+
A common performance optimization for sites with many observers is to share a signal IntersectionObserver per config and route its entries to the correct callbacks based on which element intersected. Fragment `ref`s support this same pattern through the `reactFragments` property.
538+
539+
Each first-level DOM child of a Fragment with a `ref` has a `reactFragments` property: a `Set` of `FragmentInstance` objects that contain that element. When the shared observer fires, you can use this property to look up which `FragmentInstance` owns the intersecting element and run the right callbacks.
540+
541+
```js {22,39-42}
542+
import { Fragment, useRef, useLayoutEffect } from 'react';
543+
544+
const callbackMap = new WeakMap();
545+
let cachedObserver = null;
546+
547+
function getSharedObserver(fragmentInstance, onIntersection) {
548+
// Register this callback for the fragment instance.
549+
const existing = callbackMap.get(fragmentInstance);
550+
callbackMap.set(
551+
fragmentInstance,
552+
existing ? [...existing, onIntersection] : [onIntersection],
553+
);
554+
555+
if (cachedObserver !== null) {
556+
return cachedObserver;
557+
}
558+
559+
// Create a single shared IntersectionObserver.
560+
cachedObserver = new IntersectionObserver(entries => {
561+
for (const entry of entries) {
562+
// Look up which FragmentInstances own this element.
563+
const fragmentInstances = entry.target.reactFragments;
564+
if (fragmentInstances) {
565+
for (const instance of fragmentInstances) {
566+
const callbacks = callbackMap.get(instance) || [];
567+
callbacks.forEach(cb => cb(entry));
568+
}
569+
}
570+
}
571+
});
572+
573+
return cachedObserver;
574+
}
575+
576+
function ObservedGroup({ onIntersection, children }) {
577+
const fragmentRef = useRef(null);
578+
579+
useLayoutEffect(() => {
580+
const observer = getSharedObserver(
581+
fragmentRef.current,
582+
onIntersection,
583+
);
584+
fragmentRef.current.observeUsing(observer);
585+
return () => fragmentRef.current.unobserveUsing(observer);
586+
}, [onIntersection]);
587+
588+
return (
589+
<Fragment ref={fragmentRef}>
590+
{children}
591+
</Fragment>
592+
);
593+
}
594+
```
595+
596+
With this pattern, nesting multiple `ObservedGroup` components reuses the same `IntersectionObserver`. When a child element intersects, the observer looks up all `FragmentInstance` objects on that element via `reactFragments` and calls each registered callback:
597+
598+
```js
599+
<ObservedGroup onIntersection={() => console.log('outer')}>
600+
<ObservedGroup onIntersection={() => console.log('inner')}>
601+
<div>Content</div>
602+
</ObservedGroup>
603+
</ObservedGroup>
604+
```
605+
606+
When the `<div>` becomes visible, both `'outer'` and `'inner'` are logged because the element's `reactFragments` Set contains both `FragmentInstance` objects.

0 commit comments

Comments
 (0)