Skip to content

Commit eab977f

Browse files
committed
Add cached intersection observer usage example
1 parent 6f9c54e commit eab977f

File tree

1 file changed

+76
-0
lines changed

1 file changed

+76
-0
lines changed

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

@@ -525,3 +526,78 @@ function FormFields({ children }) {
525526
```
526527
527528
`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.
529+
530+
---
531+
532+
### <CanaryBadge /> Caching a global IntersectionObserver {/*caching-global-intersection-observer*/}
533+
534+
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.
535+
536+
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.
537+
538+
```js {22,39-42}
539+
import { Fragment, useRef, useLayoutEffect } from 'react';
540+
541+
const callbackMap = new WeakMap();
542+
let cachedObserver = null;
543+
544+
function getSharedObserver(fragmentInstance, onIntersection) {
545+
// Register this callback for the fragment instance.
546+
const existing = callbackMap.get(fragmentInstance);
547+
callbackMap.set(
548+
fragmentInstance,
549+
existing ? [...existing, onIntersection] : [onIntersection],
550+
);
551+
552+
if (cachedObserver !== null) {
553+
return cachedObserver;
554+
}
555+
556+
// Create a single shared IntersectionObserver.
557+
cachedObserver = new IntersectionObserver(entries => {
558+
for (const entry of entries) {
559+
// Look up which FragmentInstances own this element.
560+
const fragmentInstances = entry.target.reactFragments;
561+
if (fragmentInstances) {
562+
for (const instance of fragmentInstances) {
563+
const callbacks = callbackMap.get(instance) || [];
564+
callbacks.forEach(cb => cb(entry));
565+
}
566+
}
567+
}
568+
});
569+
570+
return cachedObserver;
571+
}
572+
573+
function ObservedGroup({ onIntersection, children }) {
574+
const fragmentRef = useRef(null);
575+
576+
useLayoutEffect(() => {
577+
const observer = getSharedObserver(
578+
fragmentRef.current,
579+
onIntersection,
580+
);
581+
fragmentRef.current.observeUsing(observer);
582+
return () => fragmentRef.current.unobserveUsing(observer);
583+
}, [onIntersection]);
584+
585+
return (
586+
<Fragment ref={fragmentRef}>
587+
{children}
588+
</Fragment>
589+
);
590+
}
591+
```
592+
593+
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:
594+
595+
```js
596+
<ObservedGroup onIntersection={() => console.log('outer')}>
597+
<ObservedGroup onIntersection={() => console.log('inner')}>
598+
<div>Content</div>
599+
</ObservedGroup>
600+
</ObservedGroup>
601+
```
602+
603+
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)