diff --git a/.changeset/purple-baboons-rhyme.md b/.changeset/purple-baboons-rhyme.md
new file mode 100644
index 0000000..344bfb4
--- /dev/null
+++ b/.changeset/purple-baboons-rhyme.md
@@ -0,0 +1,6 @@
+---
+'react-native-bottom-tabs': patch
+'@bottom-tabs/react-navigation': patch
+---
+
+feat: add freezeOnBlur
diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock
index fd71fc1..879f9d3 100644
--- a/apps/example/ios/Podfile.lock
+++ b/apps/example/ios/Podfile.lock
@@ -1209,7 +1209,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- - react-native-bottom-tabs (0.7.3):
+ - react-native-bottom-tabs (0.7.6):
- DoubleConversion
- glog
- RCT-Folly (= 2024.01.01.00)
@@ -1222,7 +1222,7 @@ PODS:
- React-graphics
- React-ImageManager
- React-jsi
- - react-native-bottom-tabs/common (= 0.7.3)
+ - react-native-bottom-tabs/common (= 0.7.6)
- React-NativeModulesApple
- React-RCTFabric
- React-rendererdebug
@@ -1234,7 +1234,7 @@ PODS:
- SDWebImageSVGCoder (>= 1.7.0)
- SwiftUIIntrospect (~> 1.0)
- Yoga
- - react-native-bottom-tabs/common (0.7.3):
+ - react-native-bottom-tabs/common (0.7.6):
- DoubleConversion
- glog
- RCT-Folly (= 2024.01.01.00)
@@ -1943,7 +1943,7 @@ SPEC CHECKSUMS:
React-logger: d79b704bf215af194f5213a6b7deec50ba8e6a9b
React-Mapbuffer: b982d5bba94a8bc073bda48f0d27c9b28417fae3
React-microtasksnativemodule: 8fa285fed833a04a754bf575f8ded65fc240b88d
- react-native-bottom-tabs: b6b3dc2e971c860a0a6d763701929d1899f666a0
+ react-native-bottom-tabs: 084cfd4d4b1e74c03f4196b3f62d39445882f45f
react-native-safe-area-context: 73505107f7c673cd550a561aeb6271f152c483b6
React-nativeconfig: 8c83d992b9cc7d75b5abe262069eaeea4349f794
React-NativeModulesApple: b8465afc883f5bf3fe8bac3767e394d581a5f123
diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx
index b724fb2..8d6e56e 100644
--- a/apps/example/src/App.tsx
+++ b/apps/example/src/App.tsx
@@ -27,6 +27,7 @@ import TintColorsExample from './Examples/TintColors';
import NativeBottomTabsEmbeddedStacks from './Examples/NativeBottomTabsEmbeddedStacks';
import NativeBottomTabsSVGs from './Examples/NativeBottomTabsSVGs';
import NativeBottomTabsRemoteIcons from './Examples/NativeBottomTabsRemoteIcons';
+import NativeBottomTabsFreezeOnBlur from './Examples/NativeBottomTabsFreezeOnBlur';
const FourTabsIgnoreSafeArea = () => {
return ;
@@ -102,6 +103,10 @@ const examples = [
name: 'Four Tabs - Transparent scroll edge appearance',
platform: 'ios',
},
+ {
+ component: NativeBottomTabsFreezeOnBlur,
+ name: 'Native Bottom Tabs with freezeOnBlur',
+ },
{
component: FourTabsOpaqueScrollEdgeAppearance,
name: 'Four Tabs - Opaque scroll edge appearance',
diff --git a/apps/example/src/Examples/NativeBottomTabsFreezeOnBlur.tsx b/apps/example/src/Examples/NativeBottomTabsFreezeOnBlur.tsx
new file mode 100644
index 0000000..b42439a
--- /dev/null
+++ b/apps/example/src/Examples/NativeBottomTabsFreezeOnBlur.tsx
@@ -0,0 +1,115 @@
+import * as React from 'react';
+import { Platform, StyleSheet, Text, View } from 'react-native';
+import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation';
+
+const store = new Set();
+
+type Dispatch = (value: number) => void;
+
+function useValue() {
+ const [value, setValue] = React.useState(0);
+
+ React.useEffect(() => {
+ const dispatch = (value: number) => {
+ setValue(value);
+ };
+ store.add(dispatch);
+ return () => {
+ store.delete(dispatch);
+ };
+ }, [setValue]);
+
+ return value;
+}
+
+function HomeScreen() {
+ return (
+
+ Home!
+
+ );
+}
+
+function DetailsScreen(props: any) {
+ const value = useValue();
+ const screenName = props?.route?.params?.screenName;
+ // only 1 'render' should appear at the time
+ console.log(`${Platform.OS} Details Screen render ${value} ${screenName}`);
+ return (
+
+ Details!
+
+ Details Screen {value} {screenName ? screenName : ''}{' '}
+
+
+ );
+}
+const Tab = createNativeBottomTabNavigator();
+
+export default function NativeBottomTabsFreezeOnBlur() {
+ React.useEffect(() => {
+ let timer = 0;
+ const interval = setInterval(() => {
+ timer = timer + 1;
+ store.forEach((dispatch) => dispatch(timer));
+ }, 3000);
+ return () => clearInterval(interval);
+ }, []);
+
+ return (
+
+ require('../../assets/icons/article_dark.png'),
+ }}
+ />
+ require('../../assets/icons/grid_dark.png'),
+ }}
+ />
+ require('../../assets/icons/person_dark.png'),
+ }}
+ />
+ require('../../assets/icons/chat_dark.png'),
+ }}
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ screenContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+});
diff --git a/packages/react-native-bottom-tabs/package.json b/packages/react-native-bottom-tabs/package.json
index 1824df2..3427f92 100644
--- a/packages/react-native-bottom-tabs/package.json
+++ b/packages/react-native-bottom-tabs/package.json
@@ -77,11 +77,18 @@
"react": "18.3.1",
"react-native": "0.75.4",
"react-native-builder-bob": "^0.32.1",
+ "react-native-screens": "4.3.0",
"typescript": "^5.2.2"
},
"peerDependencies": {
"react": "*",
- "react-native": "*"
+ "react-native": "*",
+ "react-native-screens": ">=3.29.0"
+ },
+ "peerDependenciesMeta": {
+ "react-native-screens": {
+ "optional": true
+ }
},
"react-native-builder-bob": {
"source": "src",
diff --git a/packages/react-native-bottom-tabs/src/Screen.tsx b/packages/react-native-bottom-tabs/src/Screen.tsx
new file mode 100644
index 0000000..4e13793
--- /dev/null
+++ b/packages/react-native-bottom-tabs/src/Screen.tsx
@@ -0,0 +1,26 @@
+import * as React from 'react';
+import { View } from 'react-native';
+import type { StyleProp, ViewProps, ViewStyle } from 'react-native';
+
+interface Props extends ViewProps {
+ visible: boolean;
+ children?: React.ReactNode;
+ freezeOnBlur?: boolean;
+ style?: StyleProp;
+ collapsable?: boolean;
+}
+
+let Screens: typeof import('react-native-screens') | undefined;
+
+try {
+ Screens = require('react-native-screens');
+} catch (e) {
+ // Ignore
+}
+
+export function Screen({ visible, ...rest }: Props) {
+ if (Screens?.screensEnabled()) {
+ return ;
+ }
+ return ;
+}
diff --git a/packages/react-native-bottom-tabs/src/TabView.tsx b/packages/react-native-bottom-tabs/src/TabView.tsx
index 7f56420..72436d9 100644
--- a/packages/react-native-bottom-tabs/src/TabView.tsx
+++ b/packages/react-native-bottom-tabs/src/TabView.tsx
@@ -15,6 +15,7 @@ import type { ImageSource } from 'react-native/Libraries/Image/ImageSource';
import TabViewAdapter from './TabViewAdapter';
import useLatestCallback from 'use-latest-callback';
import type { BaseRoute, NavigationState } from './types';
+import { Screen } from './Screen';
const isAppleSymbol = (icon: any): icon is { sfSymbol: string } =>
icon?.sfSymbol;
@@ -116,6 +117,13 @@ interface Props {
*/
getTestID?: (props: { route: Route }) => string | undefined;
+ /**
+ * Get freezeOnBlur for the current screen. Uses false by default.
+ * Defaults to `true` when `enableFreeze()` is run at the top of the application.
+ *
+ */
+ getFreezeOnBlur?: (props: { route: Route }) => boolean | undefined;
+
/**
* Background color of the tab bar.
*/
@@ -160,6 +168,7 @@ const TabView = ({
tabBarInactiveTintColor: inactiveTintColor,
getLazy = ({ route }: { route: Route }) => route.lazy,
getLabelText = ({ route }: { route: Route }) => route.title,
+ getFreezeOnBlur = ({ route }: { route: Route }) => route.freezeOnBlur,
getIcon = ({ route, focused }: { route: Route; focused: boolean }) =>
route.unfocusedIcon
? focused
@@ -311,11 +320,14 @@ const TabView = ({
const focused = route.key === focusedKey;
const opacity = focused ? 1 : 0;
const zIndex = focused ? 0 : -1;
+ const freezeOnBlur = getFreezeOnBlur({ route });
return (
- ({
route,
jumpTo,
})}
-
+
);
})}
diff --git a/packages/react-native-bottom-tabs/src/types.ts b/packages/react-native-bottom-tabs/src/types.ts
index 5993a61..744af8d 100644
--- a/packages/react-native-bottom-tabs/src/types.ts
+++ b/packages/react-native-bottom-tabs/src/types.ts
@@ -15,6 +15,7 @@ export type BaseRoute = {
activeTintColor?: string;
hidden?: boolean;
testID?: string;
+ freezeOnBlur?: boolean;
};
export type NavigationState = {
diff --git a/packages/react-navigation/src/types.ts b/packages/react-navigation/src/types.ts
index eb1c3eb..3209b44 100644
--- a/packages/react-navigation/src/types.ts
+++ b/packages/react-navigation/src/types.ts
@@ -91,6 +91,14 @@ export type NativeBottomTabNavigationOptions = {
* TestID for the tab.
*/
tabBarButtonTestID?: string;
+
+ /**
+ * Whether inactive screens should be suspended from re-rendering. Defaults to `false`.
+ * Defaults to `true` when `enableFreeze()` is run at the top of the application.
+ *
+ * Only supported on iOS and Android.
+ */
+ freezeOnBlur?: boolean;
};
export type NativeBottomTabDescriptor = Descriptor<
@@ -117,5 +125,6 @@ export type NativeBottomTabNavigationConfig = Partial<
| 'onTabLongPress'
| 'getActiveTintColor'
| 'getTestID'
+ | 'getFreezeOnBlur'
>
>;
diff --git a/packages/react-navigation/src/views/NativeBottomTabView.tsx b/packages/react-navigation/src/views/NativeBottomTabView.tsx
index 8c633f4..8d2acab 100644
--- a/packages/react-navigation/src/views/NativeBottomTabView.tsx
+++ b/packages/react-navigation/src/views/NativeBottomTabView.tsx
@@ -45,6 +45,9 @@ export default function NativeBottomTabView({
const options = descriptors[route.key]?.options;
return options?.tabBarItemHidden === true;
}}
+ getFreezeOnBlur={({ route }) =>
+ descriptors[route.key]?.options.freezeOnBlur
+ }
getTestID={({ route }) =>
descriptors[route.key]?.options.tabBarButtonTestID
}
diff --git a/yarn.lock b/yarn.lock
index 0e39e01..fb2aeca 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -15614,12 +15614,14 @@ __metadata:
react: 18.3.1
react-native: 0.75.4
react-native-builder-bob: ^0.32.1
+ react-native-screens: 4.3.0
sf-symbols-typescript: ^2.0.0
typescript: ^5.2.2
use-latest-callback: ^0.2.1
peerDependencies:
react: "*"
react-native: "*"
+ react-native-screens: ">=3.29.0"
languageName: unknown
linkType: soft