Skip to main content

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.
For the complete navigation documentation, see contributingGuides/NAVIGATION.md in the repository.

Core Concepts

Navigation is defined across three main files:
  1. SCREENS.ts: Screen name constants
  2. ROUTES.ts: Route definitions and URL patterns
  3. 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
  • RIGHT_MODAL_NAVIGATOR (RHP): Most common modal for settings/details
  • ONBOARDING_MODAL_NAVIGATOR: Onboarding flow

Basic Navigation

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;

Step 3: Configure Linking

// 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.
// 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

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.

Performance Optimizations

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

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();
  }
}
import interceptAnonymousUser from '@libs/interceptAnonymousUser';

function navigateToProtectedScreen() {
  interceptAnonymousUser(() => {
    Navigation.navigate(ROUTES.SETTINGS);
  });
}
function openEditScreen() {
  Navigation.navigate(
    ROUTES.WORKSPACE_EDIT.getRoute(policyID)
  );
}

// Handle save in edit screen
function handleSave() {
  saveChanges();
  Navigation.goBack();
}

Troubleshooting

Screen Not Appearing

  1. Check that the screen is registered in the navigator
  2. Verify linking config is correct
  3. Ensure route definition matches the path
  4. Check TypeScript types are defined
  1. Verify route pattern in ROUTES.ts
  2. Check linking config path matches route
  3. Test URL format matches expected pattern
  4. Ensure screen is registered in correct navigator

Back Button Not Working

  1. Use Navigation.goBack() with fallback route
  2. Check navigation state has previous screen
  3. 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
>;
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

Build docs developers (and LLMs) love