diff --git a/docs/docs/api/safe-area-listener.mdx b/docs/docs/api/safe-area-listener.mdx
new file mode 100644
index 00000000..90196560
--- /dev/null
+++ b/docs/docs/api/safe-area-listener.mdx
@@ -0,0 +1,35 @@
+---
+sidebar_position: 2
+title: SafeAreaListener
+sidebar_label: SafeAreaListener
+---
+
+Component that lets you listen to safe area insets and frame changes at the position where it is rendered.
+
+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.
+
+### Example
+
+```tsx
+import { SafeAreaListener } from 'react-native-safe-area-context';
+
+function SomeComponent() {
+ return (
+ {
+ console.log('Safe area changed:', { insets, frame });
+ }}
+ >
+ {/* Your content here */}
+
+ );
+}
+```
+
+### Props
+
+Accepts all [View](https://reactnative.dev/view#props) props.
+
+#### `onChange`
+
+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.
diff --git a/example/src/App.tsx b/example/src/App.tsx
index aa1e26cb..2dc28436 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -2,6 +2,7 @@ import * as React from 'react';
import { DevSettings, View, Text, StatusBar } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import HooksExample from './HooksExample';
+import ListenerExample from './ListenerExample';
import SafeAreaViewExample from './SafeAreaViewExample';
import SafeAreaViewEdgesExample from './SafeAreaViewEdgesExample';
// import ReactNavigationExample from './ReactNavigationExample';
@@ -13,6 +14,7 @@ type Example =
| 'safe-area-view'
| 'safe-area-view-edges'
| 'hooks'
+ | 'listener'
| 'react-navigation'
| 'native-stack';
@@ -52,6 +54,9 @@ export default function App() {
DevSettings.addMenuItem('Show Hooks Example', () => {
setCurrentExample('hooks');
});
+ DevSettings.addMenuItem('Show Listener Example', () => {
+ setCurrentExample('listener');
+ });
DevSettings.addMenuItem('Show React Navigation Example', () => {
setCurrentExample('react-navigation');
});
@@ -71,6 +76,9 @@ export default function App() {
case 'hooks':
content = ;
break;
+ case 'listener':
+ content = ;
+ break;
// case 'react-navigation':
// content = ;
// break;
diff --git a/example/src/HooksExample.tsx b/example/src/HooksExample.tsx
index 3fdd8599..69c6ce85 100644
--- a/example/src/HooksExample.tsx
+++ b/example/src/HooksExample.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { View, Text, StatusBar, ScrollView, TextInput } from 'react-native';
+import { View, StatusBar, ScrollView, TextInput } from 'react-native';
import {
SafeAreaProvider,
@@ -7,38 +7,8 @@ import {
initialWindowMetrics,
useSafeAreaFrame,
} from 'react-native-safe-area-context';
-
-const DataView = ({ data }: { data: object | null | undefined }) => (
-
- {data == null
- ? 'null'
- : Object.entries(data)
- .map(([key, value]) => `${key}: ${value}`)
- .join('\n')}
-
-);
-
-const Card = ({
- title,
- children,
-}: {
- title: string;
- children: React.ReactNode;
-}) => (
-
-
- {title}
-
- {children}
-
-);
+import { DataView } from './components/DataView';
+import { Card } from './components/Card';
const BORDER_WIDTH = 8;
diff --git a/example/src/ListenerExample.tsx b/example/src/ListenerExample.tsx
new file mode 100644
index 00000000..4be9ebd0
--- /dev/null
+++ b/example/src/ListenerExample.tsx
@@ -0,0 +1,35 @@
+import * as React from 'react';
+import { StatusBar, TextInput } from 'react-native';
+
+import {
+ EdgeInsets,
+ Rect,
+ SafeAreaListener,
+ SafeAreaView,
+} from 'react-native-safe-area-context';
+import { DataView } from './components/DataView';
+import { Card } from './components/Card';
+
+export default function ListenerExample() {
+ const [data, setData] = React.useState<{
+ insets: EdgeInsets;
+ frame: Rect;
+ } | null>(null);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/example/src/components/Card.tsx b/example/src/components/Card.tsx
new file mode 100644
index 00000000..ba2da544
--- /dev/null
+++ b/example/src/components/Card.tsx
@@ -0,0 +1,26 @@
+import * as React from 'react';
+import { Text, View } from 'react-native';
+
+export function Card({
+ title,
+ children,
+}: {
+ title: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {title}
+
+ {children}
+
+ );
+}
diff --git a/src/SafeAreaContext.tsx b/src/SafeAreaContext.tsx
index 87ad0553..ed192d76 100644
--- a/src/SafeAreaContext.tsx
+++ b/src/SafeAreaContext.tsx
@@ -105,6 +105,32 @@ export function SafeAreaProvider({
);
}
+export interface SafeAreaListenerProps extends ViewProps {
+ onChange: (data: { insets: EdgeInsets; frame: Rect }) => void;
+}
+
+export function SafeAreaListener({
+ onChange,
+ style,
+ children,
+ ...others
+}: SafeAreaListenerProps) {
+ return (
+ {
+ onChange({
+ insets: e.nativeEvent.insets,
+ frame: e.nativeEvent.frame,
+ });
+ }}
+ >
+ {children}
+
+ );
+}
+
const styles = StyleSheet.create({
fill: { flex: 1 },
});