Skip to content

Commit

Permalink
feat: preloading for simple navigators - tabs, drawer (#11709)
Browse files Browse the repository at this point in the history
**Motivation**
This is a straightforward application of
#11702. Now, we
implement the behaviour of simple navigators. We refer to the original
PR for a detailed motivation.
Additionally, we have tests.


**Test plan**

The flow is sufficiently covered in the example app. 


https://github.com/react-navigation/react-navigation/assets/25709300/06622b0e-a0c0-4c73-8bf2-305495ddfc72
  • Loading branch information
osdnk committed Dec 7, 2023
1 parent 0b83a9b commit ad7c703
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 18 deletions.
92 changes: 92 additions & 0 deletions example/src/Screens/TabPreloadFlow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Button } from '@react-navigation/elements';
import * as React from 'react';
import { useEffect, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';

type PreloadBottomTabsParams = {
Home: undefined;
Details: undefined;
};

const DetailsScreen = ({
navigation,
}: BottomTabScreenProps<PreloadBottomTabsParams, 'Details'>) => {
const [loadingCountdown, setLoadingCountdown] = useState(3);
useEffect(() => {
const interval = setInterval(() => {
setLoadingCountdown((loadingCountdown) => {
if (loadingCountdown === 1) {
clearInterval(interval);
}
return loadingCountdown - 1;
});
}, 1000);
return () => clearInterval(interval);
}, []);

return (
<View style={styles.content}>
<Text style={[styles.text, styles.countdown]}>
{loadingCountdown > 0 && loadingCountdown}
</Text>
<Text style={styles.text}>
{loadingCountdown === 0 ? 'Loaded!' : 'Loading...'}
</Text>
<Button onPress={navigation.goBack} style={styles.button}>
Back to home
</Button>
</View>
);
};

const HomeScreen = ({
navigation,
}: BottomTabScreenProps<PreloadBottomTabsParams, 'Home'>) => {
const { navigate, preload, removePreload } = navigation;

return (
<View style={styles.content}>
<Button onPress={() => preload('Details')} style={styles.button}>
Preload screen
</Button>
<Button onPress={() => navigate('Details')} style={styles.button}>
Navigate
</Button>
<Button onPress={() => removePreload('Details')} style={styles.button}>
Remove preload
</Button>
</View>
);
};

const BottomsTabs = createBottomTabNavigator<PreloadBottomTabsParams>();

export function TabPreloadFlow() {
return (
<BottomsTabs.Navigator screenOptions={{ headerShown: false }}>
<BottomsTabs.Screen name="Home" component={HomeScreen} />
<BottomsTabs.Screen name="Details" component={DetailsScreen} />
</BottomsTabs.Navigator>
);
}

const styles = StyleSheet.create({
content: {
flex: 1,
padding: 16,
justifyContent: 'center',
},
button: {
margin: 8,
},
text: {
textAlign: 'center',
margin: 8,
},
countdown: {
fontSize: 24,
minHeight: 32,
},
});
5 changes: 5 additions & 0 deletions example/src/screens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { StackHeaderCustomization } from './Screens/StackHeaderCustomization';
import { StackPreventRemove } from './Screens/StackPreventRemove';
import { StackTransparent } from './Screens/StackTransparent';
import { StaticScreen } from './Screens/Static';
import { TabPreloadFlow } from './Screens/TabPreloadFlow';
import { TabView } from './Screens/TabView';

export type RootDrawerParamList = {
Expand Down Expand Up @@ -116,6 +117,10 @@ export const SCREENS = {
title: 'Linking with authentication flow',
component: LinkingScreen,
},
PreloadFlowTab: {
title: 'Preloading flow for Bottom Tabs',
component: TabPreloadFlow,
},
};

type ParamListTypes = {
Expand Down
31 changes: 30 additions & 1 deletion packages/bottom-tabs/src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {
createNavigationContainerRef,
NavigationContainer,
type ParamListBase,
} from '@react-navigation/native';
import { fireEvent, render } from '@testing-library/react-native';
import { act, fireEvent, render } from '@testing-library/react-native';
import * as React from 'react';
import { Animated, Button, Text, View } from 'react-native';

Expand Down Expand Up @@ -40,3 +41,31 @@ it('renders a bottom tab navigator with screens', async () => {

expect(queryByText('Screen B')).not.toBeNull();
});

type BottomTabParamList = {
A: undefined;
B: undefined;
};

it('handles screens preloading', async () => {
const Tab = createBottomTabNavigator<BottomTabParamList>();

const navigation = createNavigationContainerRef<BottomTabParamList>();

const { queryByText } = render(
<NavigationContainer ref={navigation}>
<Tab.Navigator>
<Tab.Screen name="A">{() => null}</Tab.Screen>
<Tab.Screen name="B">{() => <Text>Screen B</Text>}</Tab.Screen>
</Tab.Navigator>
</NavigationContainer>
);

expect(queryByText('Screen B', { includeHiddenElements: true })).toBeNull();
act(() => navigation.preload('B'));
expect(
queryByText('Screen B', { includeHiddenElements: true })
).not.toBeNull();
act(() => navigation.removePreload('B'));
expect(queryByText('Screen B', { includeHiddenElements: true })).toBeNull();
});
15 changes: 7 additions & 8 deletions packages/bottom-tabs/src/views/BottomTabView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,6 @@ export function BottomTabView(props: Props) {
sceneContainerStyle,
} = props;

if (state.preloadedRouteKeys.length !== 0) {
throw new Error(
'Preloading routes is not supported in the BottomTabNavigator navigator.'
);
}

const focusedRouteKey = state.routes[state.index].key;

/**
Expand Down Expand Up @@ -187,8 +181,13 @@ export function BottomTabView(props: Props) {
return null;
}

if (lazy && !loaded.includes(route.key) && !isFocused) {
// Don't render a lazy screen if we've never navigated to it
if (
lazy &&
!loaded.includes(route.key) &&
!isFocused &&
!state.preloadedRouteKeys.includes(route.key)
) {
// Don't render a lazy screen if we've never navigated to it or it wasn't preloaded
return null;
}

Expand Down
31 changes: 30 additions & 1 deletion packages/drawer/src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {
createNavigationContainerRef,
NavigationContainer,
type ParamListBase,
} from '@react-navigation/native';
import { fireEvent, render } from '@testing-library/react-native';
import { act, fireEvent, render } from '@testing-library/react-native';
import * as React from 'react';
import { Button, Text, View } from 'react-native';

Expand Down Expand Up @@ -35,3 +36,31 @@ it('renders a drawer navigator with screens', async () => {

expect(queryByText('Screen B')).not.toBeNull();
});

type DrawerParamList = {
A: undefined;
B: undefined;
};

it('handles screens preloading', async () => {
const Drawer = createDrawerNavigator<DrawerParamList>();

const navigation = createNavigationContainerRef<DrawerParamList>();

const { queryByText } = render(
<NavigationContainer ref={navigation}>
<Drawer.Navigator>
<Drawer.Screen name="A">{() => null}</Drawer.Screen>
<Drawer.Screen name="B">{() => <Text>Screen B</Text>}</Drawer.Screen>
</Drawer.Navigator>
</NavigationContainer>
);

expect(queryByText('Screen B', { includeHiddenElements: true })).toBeNull();
act(() => navigation.preload('B'));
expect(
queryByText('Screen B', { includeHiddenElements: true })
).not.toBeNull();
act(() => navigation.removePreload('B'));
expect(queryByText('Screen B', { includeHiddenElements: true })).toBeNull();
});
14 changes: 7 additions & 7 deletions packages/drawer/src/views/DrawerView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,6 @@ function DrawerViewBase({
}: Props) {
const { direction } = useLocale();

if (state.preloadedRouteKeys.length !== 0) {
throw new Error(
'Preloading routes is not supported in the DrawerNavigator navigator.'
);
}
const focusedRouteKey = state.routes[state.index].key;
const {
drawerHideStatusBarOnOpen,
Expand Down Expand Up @@ -204,8 +199,13 @@ function DrawerViewBase({
return null;
}

if (lazy && !loaded.includes(route.key) && !isFocused) {
// Don't render a lazy screen if we've never navigated to it
if (
lazy &&
!loaded.includes(route.key) &&
!isFocused &&
!state.preloadedRouteKeys.includes(route.key)
) {
// Don't render a lazy screen if we've never navigated to it or it wasn't preloaded
return null;
}

Expand Down
5 changes: 4 additions & 1 deletion packages/material-top-tabs/src/views/MaterialTopTabView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ export function MaterialTopTabView({
renderLazyPlaceholder={({ route }) =>
descriptors[route.key].options.lazyPlaceholder?.() ?? null
}
lazy={({ route }) => descriptors[route.key].options.lazy === true}
lazy={({ route }) =>
descriptors[route.key].options.lazy === true ||
state.preloadedRouteKeys.includes(route.key)
}
lazyPreloadDistance={focusedOptions.lazyPreloadDistance}
swipeEnabled={focusedOptions.swipeEnabled}
animationEnabled={focusedOptions.animationEnabled}
Expand Down

0 comments on commit ad7c703

Please sign in to comment.