diff --git a/docs/docs/docs/guides/usage-with-react-navigation.mdx b/docs/docs/docs/guides/usage-with-react-navigation.mdx index e5b5c3e..6dd1dae 100644 --- a/docs/docs/docs/guides/usage-with-react-navigation.mdx +++ b/docs/docs/docs/guides/usage-with-react-navigation.mdx @@ -174,6 +174,14 @@ Badge to show on the tab icon. 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`. Defaults to `true` when `enableFreeze()` from `react-native-screens` package is run at the top of the application. + +Requires `react-native-screens` version >=3.16.0. + +Only supported on iOS and Android. + ### Events The navigator can emit events on certain actions. Supported events are: diff --git a/example/src/App.tsx b/example/src/App.tsx index 2c53eb9..ea21e3f 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -31,6 +31,7 @@ import LabeledTabs from './Examples/Labeled'; import NativeBottomTabs from './Examples/NativeBottomTabs'; import TintColorsExample from './Examples/TintColors'; import NativeBottomTabsVectorIcons from './Examples/NativeBottomTabsVectorIcons'; +import NativeBottomTabsFreezeOnBlur from './Examples/NativeBottomTabsFreezeOnBlur'; const FourTabsIgnoreSafeArea = () => { return ; @@ -112,6 +113,10 @@ const examples = [ component: NativeBottomTabsVectorIcons, name: 'Native Bottom Tabs with Vector Icons', }, + { + component: NativeBottomTabsFreezeOnBlur, + name: 'Native Bottom Tabs with FreezeOnBlur', + }, { component: NativeBottomTabs, name: 'Native Bottom Tabs' }, { component: JSBottomTabs, name: 'JS Bottom Tabs' }, { diff --git a/example/src/Examples/NativeBottomTabsFreezeOnBlur.tsx b/example/src/Examples/NativeBottomTabsFreezeOnBlur.tsx new file mode 100644 index 0000000..f71c6ec --- /dev/null +++ b/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 '../../../src/react-navigation/navigators/createNativeBottomTabNavigator'; + +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/package.json b/package.json index 1366a0d..5bf3c2e 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "react": "18.3.1", "react-native": "0.75.3", "react-native-builder-bob": "^0.30.2", + "react-native-screens": "^3.16.0", "release-it": "^15.0.0", "turbo": "^1.10.7", "typescript": "^5.2.2" @@ -110,11 +111,15 @@ "peerDependencies": { "@react-navigation/native": ">=6", "react": "*", - "react-native": "*" + "react-native": "*", + "react-native-screens": ">=3.16.0" }, "peerDependenciesMeta": { "@react-navigation/native": { "optional": true + }, + "react-native-screens": { + "optional": true } }, "workspaces": [ diff --git a/src/TabView.tsx b/src/TabView.tsx index fe27620..7fae8f5 100644 --- a/src/TabView.tsx +++ b/src/TabView.tsx @@ -4,7 +4,6 @@ import { Image, Platform, StyleSheet, - View, processColor, } from 'react-native'; @@ -14,6 +13,10 @@ import TabViewAdapter from './TabViewAdapter'; import useLatestCallback from 'use-latest-callback'; import { useMemo, useState } from 'react'; import type { BaseRoute, NavigationState } from './types'; +import { + MaybeScreen, + MaybeScreenContainer, +} from './react-navigation/views/ScreenFallback'; const isAppleSymbol = (icon: any): icon is { sfSymbol: string } => icon?.sfSymbol; @@ -104,6 +107,13 @@ interface Props { focused: boolean; }) => ImageSource | 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. */ @@ -136,6 +146,7 @@ const TabView = ({ : route.focusedIcon, barTintColor, getActiveTintColor = ({ route }: { route: Route }) => route.activeTintColor, + getFreezeOnBlur = ({ route }: { route: Route }) => route.freezeOnBlur, tabBarActiveTintColor: activeTintColor, tabBarInactiveTintColor: inactiveTintColor, hapticFeedbackEnabled = true, @@ -238,39 +249,48 @@ const TabView = ({ barTintColor={barTintColor} rippleColor={rippleColor} > - {trimmedRoutes.map((route) => { - if (getLazy({ route }) !== false && !loaded.includes(route.key)) { - // Don't render a screen if we've never navigated to it - if (Platform.OS === 'android') { - return null; + + {trimmedRoutes.map((route) => { + const isFocused = route.key === focusedKey; + + if (getLazy({ route }) !== false && !loaded.includes(route.key)) { + // Don't render a screen if we've never navigated to it + if (Platform.OS === 'android') { + return null; + } + return ( + + ); } + + const freezeOnBlur = getFreezeOnBlur({ route }); + return ( - + style={[ + styles.fullWidth, + Platform.OS === 'android' && { + display: isFocused ? 'flex' : 'none', + }, + ]} + > + {renderScene({ + route, + jumpTo, + })} + ); - } - - return ( - - {renderScene({ - route, - jumpTo, - })} - - ); - })} + })} + ); }; @@ -280,6 +300,10 @@ const styles = StyleSheet.create({ width: '100%', height: '100%', }, + container: { + flex: 1, + overflow: 'hidden', + }, }); export default TabView; diff --git a/src/react-navigation/types.ts b/src/react-navigation/types.ts index 1d26653..fe77833 100644 --- a/src/react-navigation/types.ts +++ b/src/react-navigation/types.ts @@ -81,6 +81,15 @@ export type NativeBottomTabNavigationOptions = { * Active tab color. */ tabBarActiveTintColor?: 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. + * Requires `react-native-screens` version >=3.16.0. + * + * Only supported on iOS and Android. + */ + freezeOnBlur?: boolean; }; export type NativeBottomTabDescriptor = Descriptor< diff --git a/src/react-navigation/views/NativeBottomTabView.tsx b/src/react-navigation/views/NativeBottomTabView.tsx index 43f74ed..2ce8310 100644 --- a/src/react-navigation/views/NativeBottomTabView.tsx +++ b/src/react-navigation/views/NativeBottomTabView.tsx @@ -50,6 +50,9 @@ export default function NativeBottomTabView({ return null; }} + getFreezeOnBlur={({ route }) => + descriptors[route.key]?.options.freezeOnBlur + } getLazy={({ route }) => descriptors[route.key]?.options.lazy ?? true} onTabLongPress={(index) => { const route = state.routes[index]; diff --git a/src/react-navigation/views/ScreenFallback.tsx b/src/react-navigation/views/ScreenFallback.tsx new file mode 100644 index 0000000..a84d770 --- /dev/null +++ b/src/react-navigation/views/ScreenFallback.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { Platform, StyleProp, View, ViewProps, ViewStyle } from 'react-native'; + +type Props = { + 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 const MaybeScreenContainer = ({ children, ...rest }: ViewProps) => { + if (Platform.OS === 'android' && Screens?.screensEnabled()) { + return ( + {children} + ); + } + + return <>{children}; +}; + +export function MaybeScreen({ visible, ...rest }: Props) { + if (Screens?.screensEnabled()) { + return ; + } + return ; +} diff --git a/src/types.ts b/src/types.ts index 5ddf5aa..ff49bb8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,6 +13,7 @@ export type BaseRoute = { focusedIcon?: ImageSourcePropType | AppleIcon; unfocusedIcon?: ImageSourcePropType | AppleIcon; activeTintColor?: string; + freezeOnBlur?: boolean; }; export type NavigationState = { diff --git a/yarn.lock b/yarn.lock index c8e0aee..7f45dc4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13786,6 +13786,7 @@ __metadata: react: 18.3.1 react-native: 0.75.3 react-native-builder-bob: ^0.30.2 + react-native-screens: ^3.16.0 release-it: ^15.0.0 sf-symbols-typescript: ^2.0.0 turbo: ^1.10.7 @@ -13795,9 +13796,12 @@ __metadata: "@react-navigation/native": ">=6" react: "*" react-native: "*" + react-native-screens: ">=3.16.0" peerDependenciesMeta: "@react-navigation/native": optional: true + react-native-screens: + optional: true languageName: unknown linkType: soft @@ -13874,6 +13878,19 @@ __metadata: languageName: node linkType: hard +"react-native-screens@npm:^3.16.0": + version: 3.35.0 + resolution: "react-native-screens@npm:3.35.0" + dependencies: + react-freeze: ^1.0.0 + warn-once: ^0.1.0 + peerDependencies: + react: "*" + react-native: "*" + checksum: cb8a0c8d8a41a8a1065cc2253e4272a970366a7d80bc54e889b2f48de7ccccd3e828e2701de39c0453a67956ec0cad140fb506324cce04419b5e2eb495c038c2 + languageName: node + linkType: hard + "react-native-screens@npm:^3.34.0": version: 3.34.0 resolution: "react-native-screens@npm:3.34.0"