Skip to content

Commit a94bbfb

Browse files
rubennortefacebook-github-bot
authored andcommitted
Enable IntersectionObserver in Catalyst and RNTester (#37863)
Summary: Pull Request resolved: #37863 This creates 2 examples for IntersectionObserver in RNTester: * The first example is just a copy of the example provided by MDN in the documentation page for `IntersectionObserver` (https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). This example is useful to show how React Native behaves the same way with the same code. * The second example is a "stress test" for the API: a screen with 500 simultaneous node being observed at the same time with different observers. As we compute the intersections after scroll (after "mounting" the state update with the updated scroll position) in the main thread, this highlights a possible impact on scroll performance. IntersectionObserver isn't yet enabled by default, so no need to add a changelog entry about this. We'll add one when the API becomes generally available. Changelog: [Internal] Reviewed By: rshest Differential Revision: D45736845 fbshipit-source-id: 40b6bce39f90e04653504b1033a4edfaa65e93ca
1 parent 387bd70 commit a94bbfb

File tree

6 files changed

+327
-7
lines changed

6 files changed

+327
-7
lines changed

packages/rn-tester/js/components/RNTTestDetails.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,12 @@ function RNTTestDetails({
2929
<>
3030
{description == null ? null : (
3131
<View style={styles.section}>
32-
<Text style={styles.heading}>Description</Text>
33-
<Text style={styles.paragraph}>{description}</Text>
32+
<Text style={[styles.heading, {color: theme.LabelColor}]}>
33+
Description
34+
</Text>
35+
<Text style={[styles.paragraph, {color: theme.LabelColor}]}>
36+
{description}
37+
</Text>
3438
</View>
3539
)}
3640
{expect == null ? null : (
@@ -78,7 +82,6 @@ const styles = StyleSheet.create({
7882
},
7983
heading: {
8084
fontSize: 16,
81-
color: 'grey',
8285
fontWeight: '500',
8386
},
8487
paragraph: {
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @flow strict-local
9+
*/
10+
11+
import {RNTesterThemeContext} from '../../components/RNTesterTheme';
12+
13+
import * as React from 'react';
14+
import {
15+
useLayoutEffect,
16+
useState,
17+
useRef,
18+
type ElementRef,
19+
useContext,
20+
} from 'react';
21+
import {Button, ScrollView, StyleSheet, Text, View} from 'react-native';
22+
23+
export const name = 'IntersectionObserver Benchmark';
24+
export const title = name;
25+
export const description =
26+
'Example of using IntersectionObserver to observe a large amount of UI elements';
27+
28+
export function render(): React.Node {
29+
return <IntersectionObserverBenchark />;
30+
}
31+
32+
const ROWS = 100;
33+
const COLUMNS = 5;
34+
35+
function IntersectionObserverBenchark(): React.Node {
36+
const [isObserving, setObserving] = useState(false);
37+
38+
return (
39+
<>
40+
<View style={styles.buttonContainer}>
41+
<Button
42+
title={isObserving ? 'Stop observing' : 'Start observing'}
43+
onPress={() => setObserving(observing => !observing)}
44+
/>
45+
</View>
46+
<ScrollView>
47+
{Array(ROWS)
48+
.fill(null)
49+
.map((_, row) => (
50+
<View style={styles.row} key={row}>
51+
{Array(COLUMNS)
52+
.fill(null)
53+
.map((_2, column) => (
54+
<Item
55+
index={COLUMNS * row + column}
56+
observe={isObserving}
57+
key={column}
58+
/>
59+
))}
60+
</View>
61+
))}
62+
</ScrollView>
63+
</>
64+
);
65+
}
66+
67+
function Item({index, observe}: {index: number, observe: boolean}): React.Node {
68+
const theme = useContext(RNTesterThemeContext);
69+
const ref = useRef<?ElementRef<typeof View>>();
70+
71+
useLayoutEffect(() => {
72+
const element = ref.current;
73+
74+
if (!observe || !element) {
75+
return;
76+
}
77+
78+
const observer = new IntersectionObserver(
79+
entries => {
80+
// You can inspect the actual entries here.
81+
// We don't log them by default to avoid the logs themselves to degrade
82+
// performance.
83+
},
84+
{
85+
threshold: [0, 1],
86+
},
87+
);
88+
89+
// $FlowExpectedError
90+
observer.observe(element);
91+
92+
return () => {
93+
observer.disconnect();
94+
};
95+
}, [observe]);
96+
97+
return (
98+
<View
99+
ref={ref}
100+
style={[
101+
styles.item,
102+
{backgroundColor: theme.SecondarySystemBackgroundColor},
103+
]}>
104+
<Text style={[styles.itemText, {color: theme.LabelColor}]}>
105+
{index + 1}
106+
</Text>
107+
</View>
108+
);
109+
}
110+
111+
const styles = StyleSheet.create({
112+
buttonContainer: {
113+
padding: 10,
114+
},
115+
row: {
116+
flexDirection: 'row',
117+
},
118+
item: {
119+
flex: 1,
120+
padding: 12,
121+
margin: 5,
122+
},
123+
itemText: {
124+
fontSize: 22,
125+
textAlign: 'center',
126+
},
127+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import * as IntersectionObserverMDNExample from './IntersectionObserverMDNExample';
12+
import * as IntersectionObserverBenchmark from './IntersectionObserverBenchmark';
13+
14+
export const framework = 'React';
15+
export const title = 'IntersectionObserver';
16+
export const category = 'UI';
17+
export const documentationURL =
18+
'https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API';
19+
export const description =
20+
'API to detect paint times for elements and changes in their intersections with other elements.';
21+
export const showIndividualExamples = true;
22+
export const examples = [
23+
IntersectionObserverMDNExample,
24+
IntersectionObserverBenchmark,
25+
];
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @flow strict-local
9+
*/
10+
11+
import {RNTesterThemeContext} from '../../components/RNTesterTheme';
12+
13+
import * as React from 'react';
14+
import {
15+
useLayoutEffect,
16+
useRef,
17+
useState,
18+
type ElementRef,
19+
useContext,
20+
} from 'react';
21+
import {ScrollView, StyleSheet, Text, View} from 'react-native';
22+
23+
export const name = 'IntersectionObserver MDN Example';
24+
export const title = name;
25+
export const description =
26+
'Copy of the example in MDN about IntersectionObserver with different thresholds.';
27+
28+
export function render(): React.Node {
29+
return <IntersectionObserverMDNExample />;
30+
}
31+
32+
/**
33+
* Similar to the example in MDN: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
34+
*/
35+
function IntersectionObserverMDNExample(): React.Node {
36+
const theme = useContext(RNTesterThemeContext);
37+
38+
return (
39+
<ScrollView>
40+
<Text style={[styles.scrollDownText, {color: theme.LabelColor}]}>
41+
↓↓ Scroll down ↓↓
42+
</Text>
43+
<ListItem thresholds={buildThresholdList(100)} />
44+
<ListItem thresholds={[0.5]} initialValue={0.49} />
45+
<ListItem thresholds={buildThresholdList(10)} />
46+
<ListItem thresholds={buildThresholdList(4)} />
47+
</ScrollView>
48+
);
49+
}
50+
51+
function ListItem(props: {
52+
thresholds: Array<number>,
53+
initialValue?: number,
54+
}): React.Node {
55+
const itemRef = useRef<?ElementRef<typeof View>>(null);
56+
const [intersectionRatio, setIntersectionRatio] = useState(
57+
props.initialValue ?? 0,
58+
);
59+
60+
useLayoutEffect(() => {
61+
const itemNode = itemRef.current;
62+
if (itemNode == null) {
63+
return;
64+
}
65+
66+
const intersectionObserver = new IntersectionObserver(
67+
entries => {
68+
entries.forEach(entry => {
69+
setIntersectionRatio(entry.intersectionRatio);
70+
});
71+
},
72+
{threshold: props.thresholds},
73+
);
74+
75+
// $FlowFixMe[incompatible-call]
76+
intersectionObserver.observe(itemNode);
77+
78+
return () => {
79+
intersectionObserver.disconnect();
80+
};
81+
}, [props.thresholds]);
82+
83+
return (
84+
<View style={styles.item} ref={itemRef}>
85+
<IntersectionRatioIndicator
86+
value={intersectionRatio}
87+
style={{left: 0, top: 0}}
88+
/>
89+
<IntersectionRatioIndicator
90+
value={intersectionRatio}
91+
style={{right: 0, top: 0}}
92+
/>
93+
<IntersectionRatioIndicator
94+
value={intersectionRatio}
95+
style={{left: 0, bottom: 0}}
96+
/>
97+
<IntersectionRatioIndicator
98+
value={intersectionRatio}
99+
style={{right: 0, bottom: 0}}
100+
/>
101+
</View>
102+
);
103+
}
104+
105+
function IntersectionRatioIndicator(props: {
106+
value: number,
107+
style: {top?: number, bottom?: number, left?: number, right?: number},
108+
}): React.Node {
109+
return (
110+
<View style={[styles.intersectionRatioIndicator, props.style]}>
111+
<Text>{`${Math.floor(props.value * 100)}%`}</Text>
112+
</View>
113+
);
114+
}
115+
116+
function buildThresholdList(numSteps: number): Array<number> {
117+
const thresholds = [];
118+
119+
for (let i = 1.0; i <= numSteps; i++) {
120+
const ratio = i / numSteps;
121+
thresholds.push(ratio);
122+
}
123+
124+
thresholds.push(0);
125+
return thresholds;
126+
}
127+
128+
const styles = StyleSheet.create({
129+
scrollDownText: {
130+
textAlign: 'center',
131+
fontSize: 20,
132+
marginBottom: 700,
133+
},
134+
item: {
135+
backgroundColor: 'rgb(245, 170, 140)',
136+
borderColor: 'rgb(201, 126, 17)',
137+
borderWidth: 2,
138+
height: 500,
139+
margin: 6,
140+
},
141+
intersectionRatioIndicator: {
142+
position: 'absolute',
143+
padding: 5,
144+
backgroundColor: 'white',
145+
opacity: 0.7,
146+
borderWidth: 1,
147+
borderColor: 'black',
148+
},
149+
});

packages/rn-tester/js/utils/RNTesterList.android.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ const Components: Array<RNTesterModuleInfo> = [
132132
},
133133
];
134134

135-
const APIs: Array<RNTesterModuleInfo> = [
135+
const APIs: Array<RNTesterModuleInfo> = ([
136136
{
137137
key: 'AccessibilityExample',
138138
category: 'Basic',
@@ -188,6 +188,14 @@ const APIs: Array<RNTesterModuleInfo> = [
188188
category: 'UI',
189189
module: require('../examples/Dimensions/DimensionsExample'),
190190
},
191+
// Only show the link for the example if the API is available.
192+
typeof IntersectionObserver === 'function'
193+
? {
194+
key: 'IntersectionObserver',
195+
category: 'UI',
196+
module: require('../examples/IntersectionObserver/IntersectionObserverIndex'),
197+
}
198+
: null,
191199
{
192200
key: 'InvalidPropsExample',
193201
module: require('../examples/InvalidProps/InvalidPropsExample'),
@@ -302,7 +310,7 @@ const APIs: Array<RNTesterModuleInfo> = [
302310
category: 'Basic',
303311
module: require('../examples/Performance/PerformanceApiExample'),
304312
},
305-
];
313+
]: Array<?RNTesterModuleInfo>).filter(Boolean);
306314

307315
if (ReactNativeFeatureFlags.shouldEmitW3CPointerEvents()) {
308316
APIs.push({

packages/rn-tester/js/utils/RNTesterList.ios.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ const Components: Array<RNTesterModuleInfo> = [
138138
},
139139
];
140140

141-
const APIs: Array<RNTesterModuleInfo> = [
141+
const APIs: Array<RNTesterModuleInfo> = ([
142142
{
143143
key: 'AccessibilityExample',
144144
module: require('../examples/Accessibility/AccessibilityExample'),
@@ -194,6 +194,14 @@ const APIs: Array<RNTesterModuleInfo> = [
194194
key: 'Dimensions',
195195
module: require('../examples/Dimensions/DimensionsExample'),
196196
},
197+
// Only show the link for the example if the API is available.
198+
typeof IntersectionObserver === 'function'
199+
? {
200+
key: 'IntersectionObserver',
201+
category: 'UI',
202+
module: require('../examples/IntersectionObserver/IntersectionObserverIndex'),
203+
}
204+
: null,
197205
{
198206
key: 'InvalidPropsExample',
199207
module: require('../examples/InvalidProps/InvalidPropsExample'),
@@ -287,7 +295,7 @@ const APIs: Array<RNTesterModuleInfo> = [
287295
category: 'Basic',
288296
module: require('../examples/Performance/PerformanceApiExample'),
289297
},
290-
];
298+
]: Array<?RNTesterModuleInfo>).filter(Boolean);
291299

292300
if (ReactNativeFeatureFlags.shouldEmitW3CPointerEvents()) {
293301
APIs.push({

0 commit comments

Comments
 (0)