diff --git a/.changeset/friendly-items-invent.md b/.changeset/friendly-items-invent.md
new file mode 100644
index 00000000..0696bb8f
--- /dev/null
+++ b/.changeset/friendly-items-invent.md
@@ -0,0 +1,6 @@
+---
+'react-native-bottom-tabs': patch
+'@bottom-tabs/react-navigation': patch
+---
+
+feat: implement freezeOnBlur
diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx
index 64cf1fbd..100b97e4 100644
--- a/apps/example/src/App.tsx
+++ b/apps/example/src/App.tsx
@@ -29,6 +29,7 @@ import NativeBottomTabsSVGs from './Examples/NativeBottomTabsSVGs';
import NativeBottomTabsRemoteIcons from './Examples/NativeBottomTabsRemoteIcons';
import NativeBottomTabsUnmounting from './Examples/NativeBottomTabsUnmounting';
import NativeBottomTabsCustomTabBar from './Examples/NativeBottomTabsCustomTabBar';
+import NativeBottomTabsFreezeOnBlur from './Examples/NativeBottomTabsFreezeOnBlur';
const FourTabsIgnoreSafeArea = () => {
return ;
@@ -130,6 +131,10 @@ const examples = [
component: HiddenTab,
name: 'Four Tabs - With Hidden Tab',
},
+ {
+ component: NativeBottomTabsFreezeOnBlur,
+ name: 'Native Bottom Tabs with freezeOnBlur',
+ },
{
component: NativeBottomTabsSVGs,
name: 'Native Bottom Tabs with SVG Icons',
diff --git a/apps/example/src/Examples/NativeBottomTabs.tsx b/apps/example/src/Examples/NativeBottomTabs.tsx
index b83332b0..1fee7784 100644
--- a/apps/example/src/Examples/NativeBottomTabs.tsx
+++ b/apps/example/src/Examples/NativeBottomTabs.tsx
@@ -42,7 +42,7 @@ function NativeBottomTabs() {
console.log(
`${Platform.OS}: Long press detected on tab with key ${data.target} at the screen level.`
);
- setLabel('New Article')
+ setLabel('New Article');
},
}}
options={{
diff --git a/apps/example/src/Examples/NativeBottomTabsFreezeOnBlur.tsx b/apps/example/src/Examples/NativeBottomTabsFreezeOnBlur.tsx
new file mode 100644
index 00000000..b42439af
--- /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/apps/example/src/Examples/NativeBottomTabsSVGs.tsx b/apps/example/src/Examples/NativeBottomTabsSVGs.tsx
index e5bf6ba3..bbdb7657 100644
--- a/apps/example/src/Examples/NativeBottomTabsSVGs.tsx
+++ b/apps/example/src/Examples/NativeBottomTabsSVGs.tsx
@@ -8,7 +8,12 @@ const Tab = createNativeBottomTabNavigator();
function NativeBottomTabsSVGs() {
return (
-
+
+#### `disablePageAnimations`
Whether to disable page animations between tabs.
@@ -266,6 +266,12 @@ Due to native limitations on iOS, this option doesn't hide the tab item **when h
Whether this screens should render the first time it's accessed. Defaults to true. Set it to false if you want to render the screen on initial render.
+#### `freezeOnBlur`
+Boolean indicating whether to prevent inactive screens from re-rendering. Defaults to false.
+
+It's working separately from `enableFreeze()` in `react-native-screens`. So settings won't be shared between them.
+
+
#### `tabBarButtonTestID`
Test ID for the tab item. This can be used to find the tab item in the native view hierarchy.
diff --git a/packages/react-native-bottom-tabs/package.json b/packages/react-native-bottom-tabs/package.json
index fb453cb1..92a17d59 100644
--- a/packages/react-native-bottom-tabs/package.json
+++ b/packages/react-native-bottom-tabs/package.json
@@ -113,6 +113,7 @@
"version": "0.41.2"
},
"dependencies": {
+ "react-freeze": "^1.0.0",
"sf-symbols-typescript": "^2.0.0",
"use-latest-callback": "^0.2.1"
},
diff --git a/packages/react-native-bottom-tabs/src/DelayedFreeze.tsx b/packages/react-native-bottom-tabs/src/DelayedFreeze.tsx
new file mode 100644
index 00000000..3e49daf6
--- /dev/null
+++ b/packages/react-native-bottom-tabs/src/DelayedFreeze.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { Freeze } from 'react-freeze';
+
+interface FreezeWrapperProps {
+ freeze: boolean;
+ children: React.ReactNode;
+}
+
+// Animation delay for freeze effect.
+const ANIMATION_DELAY = 200;
+
+// This component delays the freeze effect by animation delay.
+// So that the screen is not frozen immediately causing background flash.
+function DelayedFreeze({ freeze, children }: FreezeWrapperProps) {
+ // flag used for determining whether freeze should be enabled
+ const [freezeState, setFreezeState] = React.useState(false);
+
+ React.useEffect(() => {
+ const id = setTimeout(() => {
+ setFreezeState(freeze);
+ }, ANIMATION_DELAY);
+
+ return () => {
+ clearTimeout(id);
+ };
+ }, [freeze]);
+
+ return {children};
+}
+
+export default DelayedFreeze;
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 00000000..e52d3bff
--- /dev/null
+++ b/packages/react-native-bottom-tabs/src/Screen.tsx
@@ -0,0 +1,25 @@
+import { View } from 'react-native';
+import type { ViewProps } from 'react-native';
+import DelayedFreeze from './DelayedFreeze';
+
+interface ScreenProps extends ViewProps {
+ children: React.ReactNode;
+ freeze: boolean;
+ focused?: boolean;
+}
+
+function Screen({ children, freeze, focused, ...props }: ScreenProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default Screen;
diff --git a/packages/react-native-bottom-tabs/src/TabView.tsx b/packages/react-native-bottom-tabs/src/TabView.tsx
index 865bbb8e..b7ea7d89 100644
--- a/packages/react-native-bottom-tabs/src/TabView.tsx
+++ b/packages/react-native-bottom-tabs/src/TabView.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import type { TabViewItems } from './TabViewNativeComponent';
+import Screen from './Screen';
import {
type ColorValue,
Image,
@@ -116,8 +117,16 @@ interface Props {
*/
getTestID?: (props: { route: Route }) => string | undefined;
+ /**
+ * Custom tab bar to render. Set to `null` to hide the tab bar completely.
+ */
tabBar?: () => React.ReactNode;
+ /**
+ * Get freezeOnBlur for the current screen. Uses false by default.
+ */
+ getFreezeOnBlur?: (props: { route: Route }) => boolean | undefined;
+
tabBarStyle?: {
/**
* Background color of the tab bar.
@@ -177,6 +186,7 @@ const TabView = ({
hapticFeedbackEnabled = false,
// Android's native behavior is to show labels when there are less than 4 tabs. We leave it as undefined to use the platform default behavior.
labeled = Platform.OS !== 'android' ? true : undefined,
+ getFreezeOnBlur = ({ route }: { route: Route }) => route.freezeOnBlur,
tabBar: renderCustomTabBar,
tabBarStyle,
tabLabelStyle,
@@ -317,23 +327,20 @@ const TabView = ({
}
const focused = route.key === focusedKey;
+ const freeze = !focused ? getFreezeOnBlur({ route }) : false;
return (
-
{renderScene({
route,
jumpTo,
})}
-
+
);
})}
@@ -348,6 +355,9 @@ const styles = StyleSheet.create({
height: '100%',
flex: 1,
},
+ screen: {
+ position: 'absolute',
+ },
});
export default TabView;
diff --git a/packages/react-native-bottom-tabs/src/types.ts b/packages/react-native-bottom-tabs/src/types.ts
index 5993a61c..744af8df 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 3329bb6d..4a5192fa 100644
--- a/packages/react-navigation/src/types.ts
+++ b/packages/react-navigation/src/types.ts
@@ -91,6 +91,11 @@ export type NativeBottomTabNavigationOptions = {
* TestID for the tab.
*/
tabBarButtonTestID?: string;
+
+ /**
+ * Whether inactive screens should be suspended from re-rendering. Defaults to `false`.
+ */
+ freezeOnBlur?: boolean;
};
export type NativeBottomTabDescriptor = Descriptor<
@@ -127,6 +132,7 @@ export type NativeBottomTabNavigationConfig = Partial<
| 'getActiveTintColor'
| 'getTestID'
| 'tabBar'
+ | 'getFreezeOnBlur'
>
> & {
tabBar?: (props: BottomTabBarProps) => React.ReactNode;
diff --git a/packages/react-navigation/src/views/NativeBottomTabView.tsx b/packages/react-navigation/src/views/NativeBottomTabView.tsx
index 99bd097e..b3965d16 100644
--- a/packages/react-navigation/src/views/NativeBottomTabView.tsx
+++ b/packages/react-navigation/src/views/NativeBottomTabView.tsx
@@ -63,6 +63,9 @@ export default function NativeBottomTabView({
return null;
}}
getLazy={({ route }) => descriptors[route.key]?.options.lazy ?? true}
+ getFreezeOnBlur={({ route }) =>
+ descriptors[route.key]?.options.freezeOnBlur
+ }
onTabLongPress={(index) => {
const route = state.routes[index];
if (!route) {
diff --git a/yarn.lock b/yarn.lock
index c13f294a..9e33e681 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -17105,6 +17105,7 @@ __metadata:
"@types/jest": "npm:^29.5.5"
"@types/react": "npm:^18.2.44"
react: "npm:18.3.1"
+ react-freeze: "npm:^1.0.0"
react-native: "npm:0.75.4"
react-native-builder-bob: "npm:^0.32.1"
sf-symbols-typescript: "npm:^2.0.0"