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"