Skip to content

Commit e6b8cb9

Browse files
authored
Merge pull request Expensify#65500 from software-mansion-labs/nav/route-preloading
Use route preloading functionality from react-navigation 7 to improve user experience by making switching between tabs faster and smoother
2 parents 3a61ba9 + 5be1545 commit e6b8cb9

24 files changed

Lines changed: 291 additions & 81 deletions

src/components/Navigation/NavigationTabBar/index.tsx

Lines changed: 10 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,24 @@ import useOnyx from '@hooks/useOnyx';
1717
import useResponsiveLayout from '@hooks/useResponsiveLayout';
1818
import {useSidebarOrderedReports} from '@hooks/useSidebarOrderedReports';
1919
import useStyleUtils from '@hooks/useStyleUtils';
20-
import useSubscriptionPlan from '@hooks/useSubscriptionPlan';
2120
import useTheme from '@hooks/useTheme';
2221
import useThemeStyles from '@hooks/useThemeStyles';
2322
import useWorkspacesTabIndicatorStatus from '@hooks/useWorkspacesTabIndicatorStatus';
2423
import clearSelectedText from '@libs/clearSelectedText/clearSelectedText';
2524
import getPlatform from '@libs/getPlatform';
2625
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
27-
import {getPreservedNavigatorState} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState';
28-
import {getLastVisitedTabPath, getSettingsTabStateFromSessionStorage} from '@libs/Navigation/helpers/lastVisitedTabPathUtils';
2926
import navigateToWorkspacesPage, {getWorkspaceNavigationRouteState} from '@libs/Navigation/helpers/navigateToWorkspacesPage';
30-
import {buildCannedSearchQuery, buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUtils';
3127
import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
3228
import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils';
33-
import Navigation from '@navigation/Navigation';
3429
import navigationRef from '@navigation/navigationRef';
35-
import type {RootNavigatorParamList, SearchFullscreenNavigatorParamList, State, WorkspaceSplitNavigatorParamList} from '@navigation/types';
30+
import type {WorkspaceSplitNavigatorParamList} from '@navigation/types';
3631
import NavigationTabBarAvatar from '@pages/home/sidebar/NavigationTabBarAvatar';
3732
import NavigationTabBarFloatingActionButton from '@pages/home/sidebar/NavigationTabBarFloatingActionButton';
3833
import variables from '@styles/variables';
3934
import CONST from '@src/CONST';
4035
import NAVIGATORS from '@src/NAVIGATORS';
4136
import ONYXKEYS from '@src/ONYXKEYS';
42-
import ROUTES from '@src/ROUTES';
43-
import SCREENS from '@src/SCREENS';
37+
import type SCREENS from '@src/SCREENS';
4438
import NAVIGATION_TABS from './NAVIGATION_TABS';
4539

4640
type NavigationTabBarProps = {
@@ -55,7 +49,7 @@ function NavigationTabBar({selectedTab, isTooltipAllowed = false, isTopLevelBar
5549
const {translate, preferredLocale} = useLocalize();
5650
const {indicatorColor: workspacesTabIndicatorColor, status: workspacesTabIndicatorStatus} = useWorkspacesTabIndicatorStatus();
5751
const {orderedReports} = useSidebarOrderedReports();
58-
const subscriptionPlan = useSubscriptionPlan();
52+
5953
const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: false});
6054
const navigationState = useNavigationState(findFocusedRoute);
6155
const initialNavigationRouteState = getWorkspaceNavigationRouteState();
@@ -116,7 +110,8 @@ function NavigationTabBar({selectedTab, isTooltipAllowed = false, isTopLevelBar
116110
}
117111

118112
hideInboxTooltip();
119-
Navigation.navigate(ROUTES.HOME);
113+
// We use dispatch here because the correct screens and params are preloaded and set up in usePreloadFullScreenNavigators.
114+
navigationRef.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.PUSH, payload: {name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR}});
120115
}, [hideInboxTooltip, selectedTab]);
121116

122117
const navigateToSearch = useCallback(() => {
@@ -125,27 +120,8 @@ function NavigationTabBar({selectedTab, isTooltipAllowed = false, isTopLevelBar
125120
}
126121
clearSelectedText();
127122
interceptAnonymousUser(() => {
128-
const rootState = navigationRef.getRootState() as State<RootNavigatorParamList>;
129-
const lastSearchNavigator = rootState.routes.findLast((route) => route.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR);
130-
const lastSearchNavigatorState = lastSearchNavigator && lastSearchNavigator.key ? getPreservedNavigatorState(lastSearchNavigator?.key) : undefined;
131-
const lastSearchRoute = lastSearchNavigatorState?.routes.findLast((route) => route.name === SCREENS.SEARCH.ROOT);
132-
133-
if (lastSearchRoute) {
134-
const {q, ...rest} = lastSearchRoute.params as SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.ROOT];
135-
const queryJSON = buildSearchQueryJSON(q);
136-
if (queryJSON) {
137-
const query = buildSearchQueryString(queryJSON);
138-
Navigation.navigate(
139-
ROUTES.SEARCH_ROOT.getRoute({
140-
query,
141-
...rest,
142-
}),
143-
);
144-
return;
145-
}
146-
}
147-
148-
Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery()}));
123+
// We use dispatch here because the correct screens and params are preloaded and set up in usePreloadFullScreenNavigators.
124+
navigationRef.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.PUSH, payload: {name: NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR}});
149125
});
150126
}, [selectedTab]);
151127

@@ -154,22 +130,10 @@ function NavigationTabBar({selectedTab, isTooltipAllowed = false, isTopLevelBar
154130
return;
155131
}
156132
interceptAnonymousUser(() => {
157-
const settingsTabState = getSettingsTabStateFromSessionStorage();
158-
if (settingsTabState && !shouldUseNarrowLayout) {
159-
const stateRoute = findFocusedRoute(settingsTabState);
160-
if (!subscriptionPlan && stateRoute?.name === SCREENS.SETTINGS.SUBSCRIPTION.ROOT) {
161-
Navigation.navigate(ROUTES.SETTINGS_PROFILE.route);
162-
return;
163-
}
164-
const lastVisitedSettingsRoute = getLastVisitedTabPath(settingsTabState);
165-
if (lastVisitedSettingsRoute) {
166-
Navigation.navigate(lastVisitedSettingsRoute);
167-
return;
168-
}
169-
}
170-
Navigation.navigate(ROUTES.SETTINGS);
133+
// We use dispatch here because the correct screens and params are preloaded and set up in usePreloadFullScreenNavigators.
134+
navigationRef.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.PUSH, payload: {name: NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR}});
171135
});
172-
}, [selectedTab, subscriptionPlan, shouldUseNarrowLayout]);
136+
}, [selectedTab]);
173137

174138
/**
175139
* The settings tab is related to SettingsSplitNavigator and WorkspaceSplitNavigator.

src/libs/Navigation/AppNavigator/AuthScreens.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {RouteProp} from '@react-navigation/native';
22
import {useNavigation} from '@react-navigation/native';
3+
import type {StackCardInterpolationProps} from '@react-navigation/stack';
34
import React, {memo, useEffect, useMemo, useRef, useState} from 'react';
45
import type {OnyxEntry} from 'react-native-onyx';
56
import Onyx, {withOnyx} from 'react-native-onyx';
@@ -77,6 +78,7 @@ import TestDriveModalNavigator from './Navigators/TestDriveModalNavigator';
7778
import TestToolsModalNavigator from './Navigators/TestToolsModalNavigator';
7879
import WelcomeVideoModalNavigator from './Navigators/WelcomeVideoModalNavigator';
7980
import TestDriveDemoNavigator from './TestDriveDemoNavigator';
81+
import useModalCardStyleInterpolator from './useModalCardStyleInterpolator';
8082
import useRootNavigatorScreenOptions from './useRootNavigatorScreenOptions';
8183

8284
type AuthScreensProps = {
@@ -242,6 +244,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
242244
const prevIsOnboardingLoading = usePrevious(isOnboardingLoading);
243245
const [shouldShowRequire2FAPage, setShouldShowRequire2FAPage] = useState(!!account?.needsTwoFactorAuthSetup && !account.requiresTwoFactorAuth);
244246
const navigation = useNavigation();
247+
const modalCardStyleInterpolator = useModalCardStyleInterpolator();
245248

246249
// State to track whether the delegator's authentication is completed before displaying data
247250
const [isDelegatorFromOldDotIsReady, setIsDelegatorFromOldDotIsReady] = useState(false);
@@ -477,6 +480,10 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
477480
return {
478481
...rootNavigatorScreenOptions.splitNavigator,
479482
animation: animationEnabled ? Animations.SLIDE_FROM_RIGHT : Animations.NONE,
483+
web: {
484+
...rootNavigatorScreenOptions.splitNavigator.web,
485+
cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator({props, isFullScreenModal: true, animationEnabled}),
486+
},
480487
};
481488
};
482489

@@ -543,7 +550,16 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
543550

544551
return (
545552
<ComposeProviders components={[OptionsListContextProvider, SidebarOrderedReportsContextProvider, SearchContextProvider, LockedAccountModalProvider, DelegateNoAccessModalProvider]}>
546-
<RootStack.Navigator persistentScreens={[NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, SCREENS.SEARCH.ROOT]}>
553+
<RootStack.Navigator
554+
persistentScreens={[
555+
NAVIGATORS.REPORTS_SPLIT_NAVIGATOR,
556+
NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR,
557+
NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR,
558+
NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR,
559+
SCREENS.WORKSPACES_LIST,
560+
SCREENS.SEARCH.ROOT,
561+
]}
562+
>
547563
{/* This has to be the first navigator in auth screens. */}
548564
<RootStack.Screen
549565
name={NAVIGATORS.REPORTS_SPLIT_NAVIGATOR}

src/libs/Navigation/AppNavigator/Navigators/ReportsSplitNavigator.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, {useState} from 'react';
22
import usePermissions from '@hooks/usePermissions';
33
import createSplitNavigator from '@libs/Navigation/AppNavigator/createSplitNavigator';
44
import FreezeWrapper from '@libs/Navigation/AppNavigator/FreezeWrapper';
5+
import usePreloadFullScreenNavigators from '@libs/Navigation/AppNavigator/usePreloadFullScreenNavigators';
56
import useSplitNavigatorScreenOptions from '@libs/Navigation/AppNavigator/useSplitNavigatorScreenOptions';
67
import getCurrentUrl from '@libs/Navigation/currentUrl';
78
import shouldOpenOnAdminRoom from '@libs/Navigation/helpers/shouldOpenOnAdminRoom';
@@ -37,6 +38,8 @@ function ReportsSplitNavigator({route}: PlatformStackScreenProps<AuthScreensPara
3738
return initialReport?.reportID ?? '';
3839
});
3940

41+
usePreloadFullScreenNavigators();
42+
4043
return (
4144
<FreezeWrapper>
4245
<Split.Navigator

src/libs/Navigation/AppNavigator/Navigators/SearchFullscreenNavigator.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
import usePreloadFullScreenNavigators from '@libs/Navigation/AppNavigator/usePreloadFullScreenNavigators';
23
import useSplitNavigatorScreenOptions from '@libs/Navigation/AppNavigator/useSplitNavigatorScreenOptions';
34
import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation';
45
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
@@ -18,6 +19,9 @@ const Stack = createSearchFullscreenNavigator<SearchFullscreenNavigatorParamList
1819
function SearchFullscreenNavigator({route}: PlatformStackScreenProps<AuthScreensParamList, typeof NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR>) {
1920
// These options can be used here because the full screen navigator has the same structure as the split navigator in terms of the central screens, but it does not have a sidebar.
2021
const {centralScreen: centralScreenOptions} = useSplitNavigatorScreenOptions();
22+
23+
usePreloadFullScreenNavigators();
24+
2125
return (
2226
<FreezeWrapper>
2327
<Stack.Navigator

src/libs/Navigation/AppNavigator/Navigators/SettingsSplitNavigator.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React from 'react';
33
import {View} from 'react-native';
44
import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen';
55
import createSplitNavigator from '@libs/Navigation/AppNavigator/createSplitNavigator';
6+
import usePreloadFullScreenNavigators from '@libs/Navigation/AppNavigator/usePreloadFullScreenNavigators';
67
import useSplitNavigatorScreenOptions from '@libs/Navigation/AppNavigator/useSplitNavigatorScreenOptions';
78
import type {SettingsSplitNavigatorParamList} from '@libs/Navigation/types';
89
import SCREENS from '@src/SCREENS';
@@ -29,6 +30,8 @@ function SettingsSplitNavigator() {
2930
const route = useRoute();
3031
const splitNavigatorScreenOptions = useSplitNavigatorScreenOptions();
3132

33+
usePreloadFullScreenNavigators();
34+
3235
return (
3336
<FocusTrapForScreens>
3437
<View style={{flex: 1}}>

src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {View} from 'react-native';
33
import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen';
44
import {workspaceSplitsWithoutEnteringAnimation} from '@libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers';
55
import createSplitNavigator from '@libs/Navigation/AppNavigator/createSplitNavigator';
6+
import usePreloadFullScreenNavigators from '@libs/Navigation/AppNavigator/usePreloadFullScreenNavigators';
67
import useSplitNavigatorScreenOptions from '@libs/Navigation/AppNavigator/useSplitNavigatorScreenOptions';
78
import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation';
89
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
@@ -38,6 +39,8 @@ const Split = createSplitNavigator<WorkspaceSplitNavigatorParamList>();
3839
function WorkspaceSplitNavigator({route, navigation}: PlatformStackScreenProps<AuthScreensParamList, typeof NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR>) {
3940
const splitNavigatorScreenOptions = useSplitNavigatorScreenOptions();
4041

42+
usePreloadFullScreenNavigators();
43+
4144
useEffect(() => {
4245
const unsubscribe = navigation.addListener('transitionEnd', () => {
4346
// We want to call this function only once.

src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import {StackActions} from '@react-navigation/native';
33
import type {ParamListBase, Router} from '@react-navigation/routers';
44
import SCREENS_WITH_NAVIGATION_TAB_BAR from '@components/Navigation/TopLevelNavigationTabBar/SCREENS_WITH_NAVIGATION_TAB_BAR';
55
import Log from '@libs/Log';
6+
import {isSplitNavigatorName} from '@libs/Navigation/helpers/isNavigatorName';
7+
import {SPLIT_TO_SIDEBAR} from '@libs/Navigation/linkingConfig/RELATIONS';
8+
import type {SplitNavigatorName} from '@libs/Navigation/types';
69
import NAVIGATORS from '@src/NAVIGATORS';
710
import SCREENS from '@src/SCREENS';
811
import type {OpenWorkspaceSplitActionType, PushActionType, ReplaceActionType} from './types';
@@ -79,16 +82,23 @@ function handlePushFullscreenAction(
7982
configOptions: RouterConfigOptions,
8083
stackRouter: Router<StackNavigationState<ParamListBase>, CommonActions.Action | StackActionType>,
8184
) {
82-
const stateWithNavigator = stackRouter.getStateForAction(state, action, configOptions);
85+
const actionPayloadScreen = action.payload?.params && 'screen' in action.payload.params ? (action.payload?.params?.screen as string) : undefined;
8386
const navigatorName = action.payload.name;
8487

88+
// If we navigate to the central screen of the split navigator, we need to filter this navigator from preloadedRoutes to remove a sidebar screen from the state
89+
const shouldFilterPreloadedRoutes =
90+
isSplitNavigatorName(navigatorName) &&
91+
actionPayloadScreen !== SPLIT_TO_SIDEBAR[navigatorName as SplitNavigatorName] &&
92+
state.preloadedRoutes?.some((preloadedRoute) => preloadedRoute.name === navigatorName);
93+
const adjustedState = shouldFilterPreloadedRoutes ? {...state, preloadedRoutes: state.preloadedRoutes.filter((preloadedRoute) => preloadedRoute.name !== navigatorName)} : state;
94+
const stateWithNavigator = stackRouter.getStateForAction(adjustedState, action, configOptions);
95+
8596
if (!stateWithNavigator) {
8697
Log.hmmm(`[handlePushAction] ${navigatorName} has not been found in the navigation state.`);
8798
return null;
8899
}
89100

90101
const lastFullScreenRoute = stateWithNavigator.routes.at(-1);
91-
const actionPayloadScreen = action.payload?.params && 'screen' in action.payload.params ? (action.payload?.params?.screen as string) : undefined;
92102

93103
// Transitioning to all central screens in each split should be animated
94104
if (lastFullScreenRoute?.key && actionPayloadScreen && !SCREENS_WITH_NAVIGATION_TAB_BAR.includes(actionPayloadScreen)) {

src/libs/Navigation/AppNavigator/createRootStackNavigator/index.tsx

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,12 @@
1-
import {createNavigatorFactory} from '@react-navigation/native';
21
import type {NavigationProp, NavigatorTypeBagBase, ParamListBase, StaticConfig, TypedNavigator} from '@react-navigation/native';
2+
import {createNavigatorFactory} from '@react-navigation/native';
33
import RootNavigatorExtraContent from '@components/Navigation/RootNavigatorExtraContent';
44
import useNavigationResetOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetOnLayoutChange';
5-
import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName';
65
import createPlatformStackNavigatorComponent from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent';
76
import defaultPlatformStackScreenOptions from '@libs/Navigation/PlatformStackNavigation/defaultPlatformStackScreenOptions';
8-
import type {CustomStateHookProps, PlatformStackNavigationEventMap, PlatformStackNavigationOptions, PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types';
7+
import type {PlatformStackNavigationEventMap, PlatformStackNavigationOptions, PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types';
98
import RootStackRouter from './RootStackRouter';
10-
11-
// This is an optimization to keep mounted only last few screens in the stack.
12-
function useCustomRootStackNavigatorState({state}: CustomStateHookProps) {
13-
const lastSplitIndex = state.routes.findLastIndex((route) => isFullScreenName(route.name));
14-
const routesToRender = state.routes.slice(Math.max(0, lastSplitIndex - 1), state.routes.length);
15-
16-
return {...state, routes: routesToRender, index: routesToRender.length - 1};
17-
}
9+
import useCustomRootStackNavigatorState from './useCustomRootStackNavigatorState';
1810

1911
const RootStackNavigatorComponent = createPlatformStackNavigatorComponent('RootStackNavigator', {
2012
createRouter: RootStackRouter,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName';
2+
import type {CustomStateHookProps} from '@libs/Navigation/PlatformStackNavigation/types';
3+
4+
// This is an optimization to keep mounted only last few screens in the stack.
5+
// On native platforms, we store the last two routes to handle swiping back.
6+
export default function useCustomRootStackNavigatorState({state}: CustomStateHookProps) {
7+
const lastSplitIndex = state.routes.findLastIndex((route) => isFullScreenName(route.name));
8+
const routesToRender = state.routes.slice(Math.max(0, lastSplitIndex - 1), state.routes.length);
9+
10+
return {...state, routes: routesToRender, index: routesToRender.length - 1};
11+
}

0 commit comments

Comments
 (0)