Skip to content

Commit 1a117f8

Browse files
authored
feat: add a SafeAreaListener component (#639)
This lets consumers get the insets and frame without using the hooks. So it's possible to minimize re-renders by controlling when state needs to be updated.
1 parent 94da3e4 commit 1a117f8

File tree

6 files changed

+133
-33
lines changed

6 files changed

+133
-33
lines changed

docs/docs/api/safe-area-listener.mdx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
sidebar_position: 2
3+
title: SafeAreaListener
4+
sidebar_label: SafeAreaListener
5+
---
6+
7+
Component that lets you listen to safe area insets and frame changes at the position where it is rendered.
8+
9+
This is an alternative to using the `useSafeAreaInsets` and `useSafeAreaFrame` hooks in combinations with `SafeAreaProvider`. Unlike the hooks, this notifies about changes with the `onChange` prop and doesn't trigger re-renders when the insets or frame change.
10+
11+
### Example
12+
13+
```tsx
14+
import { SafeAreaListener } from 'react-native-safe-area-context';
15+
16+
function SomeComponent() {
17+
return (
18+
<SafeAreaListener
19+
onChange={({ insets, frame }) => {
20+
console.log('Safe area changed:', { insets, frame });
21+
}}
22+
>
23+
{/* Your content here */}
24+
</SafeAreaListener>
25+
);
26+
}
27+
```
28+
29+
### Props
30+
31+
Accepts all [View](https://reactnative.dev/view#props) props.
32+
33+
#### `onChange`
34+
35+
Required, a function that receives an object with `insets` and `frame` properties. The `insets` property contains the safe area insets, and the `frame` property contains the frame of the component.

example/src/App.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22
import { DevSettings, View, Text, StatusBar } from 'react-native';
33
import AsyncStorage from '@react-native-async-storage/async-storage';
44
import HooksExample from './HooksExample';
5+
import ListenerExample from './ListenerExample';
56
import SafeAreaViewExample from './SafeAreaViewExample';
67
import SafeAreaViewEdgesExample from './SafeAreaViewEdgesExample';
78
// import ReactNavigationExample from './ReactNavigationExample';
@@ -13,6 +14,7 @@ type Example =
1314
| 'safe-area-view'
1415
| 'safe-area-view-edges'
1516
| 'hooks'
17+
| 'listener'
1618
| 'react-navigation'
1719
| 'native-stack';
1820

@@ -52,6 +54,9 @@ export default function App() {
5254
DevSettings.addMenuItem('Show Hooks Example', () => {
5355
setCurrentExample('hooks');
5456
});
57+
DevSettings.addMenuItem('Show Listener Example', () => {
58+
setCurrentExample('listener');
59+
});
5560
DevSettings.addMenuItem('Show React Navigation Example', () => {
5661
setCurrentExample('react-navigation');
5762
});
@@ -71,6 +76,9 @@ export default function App() {
7176
case 'hooks':
7277
content = <HooksExample />;
7378
break;
79+
case 'listener':
80+
content = <ListenerExample />;
81+
break;
7482
// case 'react-navigation':
7583
// content = <ReactNavigationExample />;
7684
// break;

example/src/HooksExample.tsx

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,14 @@
11
import * as React from 'react';
2-
import { View, Text, StatusBar, ScrollView, TextInput } from 'react-native';
2+
import { View, StatusBar, ScrollView, TextInput } from 'react-native';
33

44
import {
55
SafeAreaProvider,
66
useSafeAreaInsets,
77
initialWindowMetrics,
88
useSafeAreaFrame,
99
} from 'react-native-safe-area-context';
10-
11-
const DataView = ({ data }: { data: object | null | undefined }) => (
12-
<Text style={{ fontSize: 16, lineHeight: 24, color: '#292929' }}>
13-
{data == null
14-
? 'null'
15-
: Object.entries(data)
16-
.map(([key, value]) => `${key}: ${value}`)
17-
.join('\n')}
18-
</Text>
19-
);
20-
21-
const Card = ({
22-
title,
23-
children,
24-
}: {
25-
title: string;
26-
children: React.ReactNode;
27-
}) => (
28-
<View style={{ padding: 16, backgroundColor: 'white', marginBottom: 4 }}>
29-
<Text
30-
style={{
31-
fontSize: 20,
32-
fontWeight: 'bold',
33-
marginBottom: 16,
34-
color: '#292929',
35-
}}
36-
>
37-
{title}
38-
</Text>
39-
{children}
40-
</View>
41-
);
10+
import { DataView } from './components/DataView';
11+
import { Card } from './components/Card';
4212

4313
const BORDER_WIDTH = 8;
4414

example/src/ListenerExample.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as React from 'react';
2+
import { StatusBar, TextInput } from 'react-native';
3+
4+
import {
5+
EdgeInsets,
6+
Rect,
7+
SafeAreaListener,
8+
SafeAreaView,
9+
} from 'react-native-safe-area-context';
10+
import { DataView } from './components/DataView';
11+
import { Card } from './components/Card';
12+
13+
export default function ListenerExample() {
14+
const [data, setData] = React.useState<{
15+
insets: EdgeInsets;
16+
frame: Rect;
17+
} | null>(null);
18+
19+
return (
20+
<SafeAreaListener onChange={setData}>
21+
<StatusBar barStyle="dark-content" backgroundColor="transparent" />
22+
<SafeAreaView style={{ flex: 1, backgroundColor: '#eee' }}>
23+
<Card title="Input">
24+
<TextInput style={{ backgroundColor: '#eee', borderRadius: 3 }} />
25+
</Card>
26+
<Card title="Provider insets">
27+
<DataView data={data?.insets} />
28+
</Card>
29+
<Card title="Provider frame">
30+
<DataView data={data?.frame} />
31+
</Card>
32+
</SafeAreaView>
33+
</SafeAreaListener>
34+
);
35+
}

example/src/components/Card.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as React from 'react';
2+
import { Text, View } from 'react-native';
3+
4+
export function Card({
5+
title,
6+
children,
7+
}: {
8+
title: string;
9+
children: React.ReactNode;
10+
}) {
11+
return (
12+
<View style={{ padding: 16, backgroundColor: 'white', marginBottom: 4 }}>
13+
<Text
14+
style={{
15+
fontSize: 20,
16+
fontWeight: 'bold',
17+
marginBottom: 16,
18+
color: '#292929',
19+
}}
20+
>
21+
{title}
22+
</Text>
23+
{children}
24+
</View>
25+
);
26+
}

src/SafeAreaContext.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,32 @@ export function SafeAreaProvider({
105105
);
106106
}
107107

108+
export interface SafeAreaListenerProps extends ViewProps {
109+
onChange: (data: { insets: EdgeInsets; frame: Rect }) => void;
110+
}
111+
112+
export function SafeAreaListener({
113+
onChange,
114+
style,
115+
children,
116+
...others
117+
}: SafeAreaListenerProps) {
118+
return (
119+
<NativeSafeAreaProvider
120+
{...others}
121+
style={[styles.fill, style]}
122+
onInsetsChange={(e) => {
123+
onChange({
124+
insets: e.nativeEvent.insets,
125+
frame: e.nativeEvent.frame,
126+
});
127+
}}
128+
>
129+
{children}
130+
</NativeSafeAreaProvider>
131+
);
132+
}
133+
108134
const styles = StyleSheet.create({
109135
fill: { flex: 1 },
110136
});

0 commit comments

Comments
 (0)