Documentation Index Fetch the complete documentation index at: https://mintlify.com/Expensify/App/llms.txt
Use this file to discover all available pages before exploring further.
Overview
New Expensify uses React Navigation with custom navigators and deep linking to provide a seamless cross-platform navigation experience. The navigation system is designed to work identically on iOS, Android, Web, and Desktop.
Core Concepts
Navigation Files
Navigation is defined across three main files:
SCREENS.ts : Screen name constants
ROUTES.ts : Route definitions and URL patterns
NAVIGATORS.ts : Navigator name constants
// SCREENS.ts
const SCREENS = {
SETTINGS: {
ROOT: 'Settings_Root' ,
PROFILE: {
ROOT: 'Settings_Profile' ,
},
},
} as const ;
// ROUTES.ts
const ROUTES = {
SETTINGS: 'settings' ,
SETTINGS_PROFILE: 'settings/profile' ,
} as const ;
// NAVIGATORS.ts
const NAVIGATORS = {
SETTINGS_SPLIT_NAVIGATOR: 'SettingsSplitNavigator' ,
RIGHT_MODAL_NAVIGATOR: 'RightModalNavigator' ,
} as const ;
Key Navigators
RootStackNavigator
The top-level navigator that manages the main navigation stack.
SplitNavigator
Custom navigator for wide screens with sidebar and central pane:
LHN (Left Hand Navigation) : Report list on the left
Central Pane : Main content area
RHP (Right Hand Panel) : Settings and detail panels
Examples:
NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR: Account tab
NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR: Workspaces tab
NAVIGATORS.REPORTS_SPLIT_NAVIGATOR: Inbox tab
Modal Navigators
RIGHT_MODAL_NAVIGATOR (RHP) : Most common modal for settings/details
ONBOARDING_MODAL_NAVIGATOR : Onboarding flow
Basic Navigation
Navigating Between Screens
import Navigation from '@libs/Navigation/Navigation' ;
import ROUTES from '@src/ROUTES' ;
// Navigate to a screen
Navigation . navigate ( ROUTES . SETTINGS );
// Navigate with parameters
Navigation . navigate (
ROUTES . WORKSPACE_OVERVIEW . getRoute ( policyID )
);
// Navigate and replace current screen
Navigation . navigate ( ROUTES . SETTINGS , { forceReplace: true });
Going Back
// Simple back navigation
Navigation . goBack ();
// Back with fallback route
Navigation . goBack ( ROUTES . SETTINGS );
// Back to specific screen with parameters
Navigation . goBack (
ROUTES . WORKSPACE_OVERVIEW . getRoute ( policyID )
);
Dismissing Modals
// Close RHP modal
Navigation . dismissModal ();
// Close modal and navigate to report
Navigation . dismissModalWithReport ({
reportID: '123' ,
});
Defining Routes
Static Routes
Simple routes without parameters:
// ROUTES.ts
const ROUTES = {
SETTINGS: 'settings' ,
INBOX: 'inbox' ,
} as const ;
Dynamic Routes
Routes with parameters:
// ROUTES.ts
const ROUTES = {
REPORT_WITH_ID: {
route: 'r/:reportID' ,
getRoute : ( reportID : string ) => `r/ ${ reportID } ` as const ,
},
WORKSPACE_OVERVIEW: {
route: 'workspaces/:policyID/overview' ,
getRoute : ( policyID : string ) => `workspaces/ ${ policyID } /overview` as const ,
},
} as const ;
// Usage
Navigation . navigate ( ROUTES . REPORT_WITH_ID . getRoute ( '12345' ));
Adding a New Screen
Step 1: Define Screen Name
// SCREENS.ts
const SCREENS = {
SETTINGS: {
// ... existing screens
NEW_SCREEN: 'Settings_NewScreen' ,
},
} as const ;
Step 2: Define Route
// ROUTES.ts
const ROUTES = {
// ... existing routes
SETTINGS_NEW_SCREEN: 'settings/new-screen' ,
// Or with parameters
SETTINGS_NEW_SCREEN: {
route: 'settings/new-screen/:id' ,
getRoute : ( id : string ) => `settings/new-screen/ ${ id } ` as const ,
},
} as const ;
// src/libs/Navigation/linkingConfig/config.ts
const config : LinkingOptions < RootNavigatorParamList >[ 'config' ] = {
screens: {
[ NAVIGATORS . SETTINGS_SPLIT_NAVIGATOR ]: {
screens: {
[ SCREENS . SETTINGS . NEW_SCREEN ]: {
path: ROUTES . SETTINGS_NEW_SCREEN ,
exact: true ,
},
},
},
},
};
Step 4: Define TypeScript Types
// src/libs/Navigation/types.ts
type SettingsSplitNavigatorParamList = {
// ... existing types
[ SCREENS . SETTINGS . NEW_SCREEN ] : undefined ; // No params
// Or with params:
[ SCREENS . SETTINGS . NEW_SCREEN ] : { id : string };
};
Step 5: Create Screen Component
// src/pages/settings/NewScreen.tsx
import ScreenWrapper from '@components/ScreenWrapper' ;
import HeaderWithBackButton from '@components/HeaderWithBackButton' ;
import type { PlatformStackScreenProps } from '@libs/Navigation/PlatformStackNavigation/types' ;
import type { SettingsSplitNavigatorParamList } from '@libs/Navigation/types' ;
import SCREENS from '@src/SCREENS' ;
type NewScreenProps = PlatformStackScreenProps <
SettingsSplitNavigatorParamList ,
typeof SCREENS . SETTINGS . NEW_SCREEN
>;
function NewScreen ({ route } : NewScreenProps ) {
const { id } = route . params ;
return (
< ScreenWrapper testID = "NewScreen" >
< HeaderWithBackButton title = "New Screen" />
< Text > Screen ID : { id }</ Text >
</ ScreenWrapper >
);
}
export default NewScreen ;
Step 6: Register in Navigator
// src/libs/Navigation/AppNavigator/Navigators/SettingsSplitNavigator.tsx
import type ReactComponentModule from '@src/types/utils/ReactComponentModule' ;
const CENTRAL_PANE_SETTINGS_SCREENS = {
// ... existing screens
[ SCREENS . SETTINGS . NEW_SCREEN ]: () =>
require < ReactComponentModule >( '../../../../pages/settings/NewScreen' ). default ,
} satisfies Screens ;
Deep Linking
New Expensify supports deep linking for all screens:
https://new.expensify.com/settings/profile
https://new.expensify.com/r/12345
https://new.expensify.com/workspaces/67890/overview
The navigation state is automatically reconstructed from the URL.
Handling Deep Links
// Deep links automatically navigate to the correct screen
// No special handling needed in most cases
// For special cases, use interceptAnonymousUser
import interceptAnonymousUser from '@libs/interceptAnonymousUser' ;
interceptAnonymousUser (() => {
Navigation . navigate ( ROUTES . SETTINGS );
});
// Redirects to login if user is not authenticated
Navigation State
Accessing Current Route
import Navigation from '@libs/Navigation/Navigation' ;
const currentRoute = Navigation . getActiveRoute ();
console . log ( 'Current route:' , currentRoute );
Using Navigation State in Components
import { useRoute } from '@react-navigation/native' ;
import type { PlatformStackRouteProp } from '@libs/Navigation/PlatformStackNavigation/types' ;
function MyComponent () {
const route = useRoute < PlatformStackRouteProp < ParamList , ScreenName >>();
console . log ( 'Route params:' , route . params );
console . log ( 'Route name:' , route . name );
return < View />;
}
Multi-Step Flows
For wizards and multi-step forms, use the useSubPage hook:
import useSubPage from '@hooks/useSubPage' ;
import type { SubPageProps } from '@hooks/useSubPage/types' ;
const pages = [
{ pageName: 'step-1' , component: Step1 },
{ pageName: 'step-2' , component: Step2 },
{ pageName: 'step-3' , component: Step3 },
];
function MyFlowContent () {
const {
CurrentPage ,
isEditing ,
pageIndex ,
nextPage ,
prevPage ,
moveTo ,
} = useSubPage < SubPageProps >({
pages ,
startFrom: 0 ,
onFinished : () => Navigation . goBack (),
buildRoute : ( pageName , action ) =>
ROUTES . MY_FLOW . getRoute ( pageName , action ),
});
return (
< ScreenWrapper >
< HeaderWithBackButton
onBackButtonPress = { pageIndex === 0 ? Navigation . goBack : prevPage }
/>
< CurrentPage
isEditing = { isEditing }
onNext = { nextPage }
onMove = { moveTo }
/>
</ ScreenWrapper >
);
}
RHP (Right Hand Panel)
When to Use RHP
Use RHP for:
Settings screens
Detail views
Edit forms
Secondary flows
Setting Underlay Screen
Configure which screen appears underneath RHP:
// src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts
const SETTINGS_TO_RHP : Partial < Record < keyof SettingsSplitNavigatorParamList , string []>> = {
[ SCREENS . SETTINGS . PROFILE . ROOT ]: [
SCREENS . SETTINGS . DISPLAY_NAME ,
SCREENS . SETTINGS . TIMEZONE ,
],
};
When opening DISPLAY_NAME in RHP, PROFILE.ROOT will show underneath.
The navigation system includes several performance optimizations:
1. Limited Screen Mounting
Only the last 2 screens in the stack are mounted to reduce memory usage.
2. Preserved State
Unmounted screen state is preserved and restored when navigating back.
3. Session Storage
Last visited paths are saved to session storage for page refresh persistence:
// Automatically handled by the navigation system
// Last visited paths are restored on refresh
Navigation Testing
Testing Navigation in Components
import { render } from '@testing-library/react-native' ;
import Navigation from '@libs/Navigation/Navigation' ;
jest . mock ( '@libs/Navigation/Navigation' );
test ( 'navigates on button press' , () => {
const { getByText } = render (< MyComponent />);
fireEvent . press ( getByText ( 'Settings' ));
expect ( Navigation . navigate ). toHaveBeenCalledWith ( ROUTES . SETTINGS );
});
Testing Route Parameters
import { useRoute } from '@react-navigation/native' ;
jest . mock ( '@react-navigation/native' );
test ( 'displays route parameter' , () => {
( useRoute as jest . Mock ). mockReturnValue ({
params: { reportID: '123' },
});
const { getByText } = render (< ReportScreen />);
expect ( getByText ( 'Report ID: 123' )). toBeTruthy ();
});
Common Patterns
Conditional Navigation
function handleNavigate () {
if ( hasUnsavedChanges ) {
// Show confirmation modal
showConfirmationModal ();
} else {
Navigation . goBack ();
}
}
Navigation with Authentication
import interceptAnonymousUser from '@libs/interceptAnonymousUser' ;
function navigateToProtectedScreen () {
interceptAnonymousUser (() => {
Navigation . navigate ( ROUTES . SETTINGS );
});
}
Navigation with Callback
function openEditScreen () {
Navigation . navigate (
ROUTES . WORKSPACE_EDIT . getRoute ( policyID )
);
}
// Handle save in edit screen
function handleSave () {
saveChanges ();
Navigation . goBack ();
}
Troubleshooting
Screen Not Appearing
Check that the screen is registered in the navigator
Verify linking config is correct
Ensure route definition matches the path
Check TypeScript types are defined
Deep Link Not Working
Verify route pattern in ROUTES.ts
Check linking config path matches route
Test URL format matches expected pattern
Ensure screen is registered in correct navigator
Use Navigation.goBack() with fallback route
Check navigation state has previous screen
Verify backToRoute is set correctly for deep links
Best Practices
1. Always Use Route Constants
// ❌ Bad
Navigation . navigate ( 'settings/profile' );
// ✅ Good
Navigation . navigate ( ROUTES . SETTINGS_PROFILE );
2. Provide Fallback Routes
// ❌ Bad
Navigation . goBack ();
// ✅ Good
Navigation . goBack ( ROUTES . SETTINGS );
3. Use TypeScript Types
type ScreenProps = PlatformStackScreenProps <
NavigatorParamList ,
typeof SCREENS . SCREEN_NAME
>;
4. Handle Deep Links
Ensure screens work when opened directly via URL.
Next Steps
Offline-First Understand offline navigation patterns
Testing Test navigation flows
API Integration Navigate after API calls
Full Navigation Guide Complete navigation documentation