Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/docs/docs/guides/usage-with-react-navigation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ Tab views using the sidebar adaptable style have an appearance

Whether to enable haptic feedback on tab press. Defaults to true.

#### `detachInactiveScreens`

Boolean used to indicate whether inactive screens should be detached from the view hierarchy to save memory. This enables integration with `react-native-screens`. Defaults to `true`.

### Options

The following options can be used to configure the screens in the navigator. These can be specified under `screenOptions` prop of `Tab.navigator` or `options` prop of `Tab.Screen`.
Expand Down Expand Up @@ -174,6 +178,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:
Expand Down
5 changes: 5 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <FourTabs ignoresTopSafeArea />;
Expand Down Expand Up @@ -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' },
{
Expand Down
100 changes: 100 additions & 0 deletions example/src/Examples/NativeBottomTabsFreezeOnBlur.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
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<Dispatch>();

type Dispatch = (value: number) => void;

function useValue() {
const [value, setValue] = React.useState<number>(0);

React.useEffect(() => {
const dispatch = (value: number) => {
setValue(value);
};
store.add(dispatch);
return () => {
store.delete(dispatch);
};
}, [setValue]);

return value;
}

function HomeScreen() {
return (
<View style={styles.screenContainer}>
<Text>Home!</Text>
</View>
);
}

function DetailsScreen() {
const value = useValue();
// only 1 'render' should appear at the time
console.log(`${Platform.OS} Details Screen render ${value}`);
return (
<View style={styles.screenContainer}>
<Text>Details!</Text>
<Text style={{ alignSelf: 'center' }}>Details Screen {value}</Text>
</View>
);
}
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 (
<Tab.Navigator
screenOptions={{
freezeOnBlur: true,
}}
>
<Tab.Screen
name="Article"
component={HomeScreen}
options={{
tabBarIcon: () => require('../../assets/icons/article_dark.png'),
}}
/>
<Tab.Screen
name="Albums"
component={DetailsScreen}
options={{
tabBarIcon: () => require('../../assets/icons/grid_dark.png'),
}}
/>
<Tab.Screen
name="Contact"
component={DetailsScreen}
options={{
tabBarIcon: () => require('../../assets/icons/person_dark.png'),
}}
/>
<Tab.Screen
name="Chat"
component={DetailsScreen}
options={{
tabBarIcon: () => require('../../assets/icons/chat_dark.png'),
}}
/>
</Tab.Navigator>
);
}

const styles = StyleSheet.create({
screenContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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": [
Expand Down
98 changes: 69 additions & 29 deletions src/TabView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
Image,
Platform,
StyleSheet,
View,
processColor,
} from 'react-native';

Expand All @@ -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;
Expand Down Expand Up @@ -104,6 +107,13 @@ interface Props<Route extends BaseRoute> {
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.
*/
Expand All @@ -117,6 +127,13 @@ interface Props<Route extends BaseRoute> {
* Color of tab indicator. (Android only)
*/
activeIndicatorColor?: ColorValue;

/**
* Whether inactive screens should be detached from the view hierarchy to save memory.
* Make sure to call `enableScreens` from `react-native-screens` to make it work.
* Defaults to `true` on Android.
*/
detachInactiveScreens?: boolean;
}

const ANDROID_MAX_TABS = 6;
Expand All @@ -136,6 +153,10 @@ const TabView = <Route extends BaseRoute>({
: route.focusedIcon,
barTintColor,
getActiveTintColor = ({ route }: { route: Route }) => route.activeTintColor,
getFreezeOnBlur = ({ route }: { route: Route }) => route.freezeOnBlur,
detachInactiveScreens = Platform.OS === 'web' ||
Platform.OS === 'android' ||
Platform.OS === 'ios',
tabBarActiveTintColor: activeTintColor,
tabBarInactiveTintColor: inactiveTintColor,
hapticFeedbackEnabled = true,
Expand Down Expand Up @@ -238,39 +259,54 @@ const TabView = <Route extends BaseRoute>({
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;
<MaybeScreenContainer
enabled={detachInactiveScreens}
hasTwoStates
style={styles.container}
>
Copy link
Member

@okwasniewski okwasniewski Nov 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tested it on both new and old architecture? I feel like this might break passing children on new arch as technically now we have only one child.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Till now I tested this on Android for both new and old arch.
I will be testing it on iOS today.

{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 (
<MaybeScreen
key={route.key}
visible={isFocused}
enabled={detachInactiveScreens}
style={styles.fullWidth}
collapsable={false}
/>
);
}

const freezeOnBlur = getFreezeOnBlur({ route });

return (
<View
<MaybeScreen
key={route.key}
visible={isFocused}
enabled={detachInactiveScreens}
freezeOnBlur={freezeOnBlur}
collapsable={false}
style={styles.fullWidth}
/>
style={[
styles.fullWidth,
Platform.OS === 'android' && {
display: isFocused ? 'flex' : 'none',
},
]}
>
{renderScene({
route,
jumpTo,
})}
</MaybeScreen>
);
}

return (
<View
key={route.key}
collapsable={false}
style={[
styles.fullWidth,
Platform.OS === 'android' && {
display: route.key === focusedKey ? 'flex' : 'none',
},
]}
>
{renderScene({
route,
jumpTo,
})}
</View>
);
})}
})}
</MaybeScreenContainer>
</TabViewAdapter>
);
};
Expand All @@ -280,6 +316,10 @@ const styles = StyleSheet.create({
width: '100%',
height: '100%',
},
container: {
flex: 1,
overflow: 'hidden',
},
});

export default TabView;
9 changes: 9 additions & 0 deletions src/react-navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down
3 changes: 3 additions & 0 deletions src/react-navigation/views/NativeBottomTabView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
46 changes: 46 additions & 0 deletions src/react-navigation/views/ScreenFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as React from 'react';
import { Platform, StyleProp, View, ViewProps, ViewStyle } from 'react-native';

type Props = {
visible: boolean;
children?: React.ReactNode;
enabled: boolean;
freezeOnBlur?: boolean;
style?: StyleProp<ViewStyle>;
collapsable?: boolean;
};

let Screens: typeof import('react-native-screens') | undefined;

try {
Screens = require('react-native-screens');
} catch (e) {
// Ignore
}

export const MaybeScreenContainer = ({
enabled,
children,
...rest
}: ViewProps & {
enabled: boolean;
hasTwoStates: boolean;
children?: React.ReactNode;
}) => {
if (Platform.OS === 'android' && Screens?.screensEnabled()) {
return (
<Screens.ScreenContainer enabled={enabled} {...rest}>
{children}
</Screens.ScreenContainer>
);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we don't have the navigation container on iOS (JS Tabs have it) but it works without it?

That's weird 😅

If the ScreenContainer is required then adding this might not be possible. As we need to have multiple children here

@tboba Could you help here? 🙏

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@okwasniewski Yeah on iOS it works even without Screen Container.

In case of Android we need ScreenContainer and I just checked that It worked even with hasTwoStates={false} on both platforms with New and Old Arch.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, @shubhamguptadream11 after consulting with react-native-screens team (thanks @kkafar, @tboba) it looks like detachInactiveScreens won't work without the ScreenContainer.

Can you change the PR to only add freezeOnBlur?


return <>{children}</>;
};

export function MaybeScreen({ visible, ...rest }: Props) {
if (Screens?.screensEnabled()) {
return <Screens.Screen activityState={visible ? 2 : 0} {...rest} />;
}
return <View {...rest} />;
}
Loading