Skip to main content

React Native Navigation

Comprehensive guide to navigation patterns with React Navigation, including type-safe routing, authentication flows, and deep linking.

Overview

React Navigation is the standard navigation library for React Native. This guide covers navigation setup, type-safe patterns, authentication flows, and banking-specific navigation requirements like biometric re-authentication on app resume.

For foundational navigation concepts (stack, tab, drawer, modal patterns), see Mobile Navigation. This guide focuses on React Navigation-specific implementation details.

React Navigation is a JavaScript-based navigation solution that manages navigation state, screen transitions, and routing entirely in JavaScript. Unlike native navigation solutions (UINavigationController on iOS, FragmentManager on Android), React Navigation keeps navigation logic unified across platforms. This means the same navigation code works identically on iOS and Android, while still using native platform animations and gestures under the hood.

The library uses a stack-based navigation model where screens are pushed onto and popped from a stack, similar to how web browsers manage history. However, React Navigation extends this with tabs, drawers, and modals, allowing you to compose complex navigation hierarchies. For banking apps, a typical structure is: a root stack that switches between authentication and main app, bottom tabs for primary features (payments, transactions, profile), and nested stacks within each tab for drill-down navigation (payment list → payment detail → edit payment).

Type safety in navigation is critical because navigation parameters often carry sensitive data: account IDs, transaction amounts, recipient details. A typo in a parameter name or passing a number instead of a string can lead to runtime crashes or, worse, incorrect data being displayed or processed. TypeScript's compile-time checking prevents these errors before code reaches production. See TypeScript Guidelines for our strict typing standards that ensure navigation type safety.


Core Principles

  1. Type-Safe Navigation: Define all route parameters with TypeScript
  2. Authentication-Aware: Different navigation trees for authenticated/unauthenticated states
  3. Deep Linking: Support payment links, transaction details from notifications
  4. State Persistence: Save/restore navigation state for offline scenarios
  5. Performance: Lazy load screens, optimize transitions
  6. Accessibility: Screen reader announcements for route changes


Root Navigator with Authentication

// navigation/RootNavigator.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useAuth } from '../hooks/useAuth';
import AuthNavigator from './AuthNavigator';
import AppNavigator from './AppNavigator';

export type RootStackParamList = {
Auth: undefined;
App: undefined;
};

const Stack = createNativeStackNavigator<RootStackParamList>();

export default function RootNavigator() {
const { isAuthenticated } = useAuth();

return (
<NavigationContainer>
<Stack.Navigator screenOptions={{ headerShown: false }}>
{isAuthenticated ? (
<Stack.Screen name="App" component={AppNavigator} />
) : (
<Stack.Screen name="Auth" component={AuthNavigator} />
)}
</Stack.Navigator>
</NavigationContainer>
);
}

Type-Safe Navigation Flow


Stack Navigation

Payment Stack with Type Safety

// navigation/PaymentStackNavigator.tsx
import React from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import PaymentListScreen from '../screens/payments/PaymentListScreen';
import PaymentDetailScreen from '../screens/payments/PaymentDetailScreen';
import CreatePaymentScreen from '../screens/payments/CreatePaymentScreen';
import ConfirmPaymentScreen from '../screens/payments/ConfirmPaymentScreen';

/**
* Type definition for Payment Stack routes
* Each route must specify its parameters
*/
export type PaymentStackParamList = {
PaymentList: undefined;
PaymentDetail: { paymentId: string };
CreatePayment: { recipientId?: string; amount?: string };
ConfirmPayment: {
amount: string;
recipientName: string;
recipientAccount: string;
};
};

const Stack = createNativeStackNavigator<PaymentStackParamList>();

export default function PaymentStackNavigator() {
return (
<Stack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: '#007AFF',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
}}
>
<Stack.Screen
name="PaymentList"
component={PaymentListScreen}
options={{ title: 'Payments' }}
/>
<Stack.Screen
name="PaymentDetail"
component={PaymentDetailScreen}
options={{ title: 'Payment Details' }}
/>
<Stack.Screen
name="CreatePayment"
component={CreatePaymentScreen}
options={{
title: 'New Payment',
presentation: 'modal', // iOS modal presentation
}}
/>
<Stack.Screen
name="ConfirmPayment"
component={ConfirmPaymentScreen}
options={{
title: 'Confirm Payment',
gestureEnabled: false, // Prevent swipe back
}}
/>
</Stack.Navigator>
);
}

Type-Safe Screen Implementation

// types/navigation.ts
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { RouteProp } from '@react-navigation/native';
import type { PaymentStackParamList } from '../navigation/PaymentStackNavigator';

export type PaymentDetailNavigationProp = NativeStackNavigationProp<
PaymentStackParamList,
'PaymentDetail'
>;

export type PaymentDetailRouteProp = RouteProp<
PaymentStackParamList,
'PaymentDetail'
>;

// Screen implementation
import React, { useEffect } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { useNavigation, useRoute } from '@react-navigation/native';
import type { PaymentDetailNavigationProp, PaymentDetailRouteProp } from '../../types/navigation';

export default function PaymentDetailScreen() {
const navigation = useNavigation<PaymentDetailNavigationProp>();
const route = useRoute<PaymentDetailRouteProp>();

// TypeScript knows paymentId is a string
const { paymentId } = route.params;

const handleEdit = () => {
// TypeScript validates parameter types
navigation.navigate('CreatePayment', {
recipientId: 'recipient-123',
});
};

return (
<View style={styles.container}>
<Text>Payment ID: {paymentId}</Text>
<Button title="Edit" onPress={handleEdit} />
</View>
);
}

Tab Navigation

Bottom Tab Navigator

// navigation/BottomTabNavigator.tsx
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Icon from 'react-native-vector-icons/Ionicons';
import PaymentStackNavigator from './PaymentStackNavigator';
import ProfileStackNavigator from './ProfileStackNavigator';
import TransactionStackNavigator from './TransactionStackNavigator';

export type BottomTabParamList = {
PaymentsTab: undefined;
TransactionsTab: undefined;
ProfileTab: undefined;
};

const Tab = createBottomTabNavigator<BottomTabParamList>();

export default function BottomTabNavigator() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName: string;

if (route.name === 'PaymentsTab') {
iconName = focused ? 'wallet' : 'wallet-outline';
} else if (route.name === 'TransactionsTab') {
iconName = focused ? 'list' : 'list-outline';
} else if (route.name === 'ProfileTab') {
iconName = focused ? 'person' : 'person-outline';
} else {
iconName = 'help-outline';
}

return <Icon name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#007AFF',
tabBarInactiveTintColor: '#8E8E93',
headerShown: false,
})}
>
<Tab.Screen
name="PaymentsTab"
component={PaymentStackNavigator}
options={{ tabBarLabel: 'Payments' }}
/>
<Tab.Screen
name="TransactionsTab"
component={TransactionStackNavigator}
options={{ tabBarLabel: 'Transactions' }}
/>
<Tab.Screen
name="ProfileTab"
component={ProfileStackNavigator}
options={{ tabBarLabel: 'Profile' }}
/>
</Tab.Navigator>
);
}

Deep Linking

Deep linking allows external sources (push notifications, emails, SMS, web links) to navigate directly to specific screens within your app. For deep linking concepts and patterns, see Mobile Navigation - Deep Linking. This section covers React Navigation-specific implementation.

React Navigation's deep linking configuration maps URL patterns to navigation states. The prefixes array defines URL schemes your app responds to: custom schemes (bankapp://) work from any source, while universal links (https://app.bank.com) allow your website to open directly in your app if installed. Universal links are preferred for banking apps because they provide a seamless web-to-app transition and work more reliably across platforms.

The URL pattern matching uses a hierarchical structure that mirrors your navigation hierarchy. A URL like bankapp://payments/abc123 matches the PaymentDetail screen pattern payments/:paymentId, extracting abc123 as the paymentId parameter. React Navigation automatically navigates through the necessary stack (Auth → App → PaymentsTab → PaymentDetail) and passes the extracted parameters to the destination screen.

Security consideration: Always validate deep link parameters before using them. A malicious actor could craft a URL like bankapp://payments/../../../admin to attempt path traversal. Validate that IDs match expected formats (UUIDs, numeric IDs) and verify permissions before displaying sensitive data. See Input Validation for validation patterns.

// navigation/linking.ts
import { LinkingOptions } from '@react-navigation/native';
import type { RootStackParamList } from './RootNavigator';

export const linking: LinkingOptions<RootStackParamList> = {
prefixes: ['bankapp://', 'https://app.bank.com'],
config: {
screens: {
Auth: {
screens: {
Login: 'login',
BiometricAuth: 'auth/biometric',
},
},
App: {
screens: {
PaymentsTab: {
screens: {
PaymentList: 'payments',
PaymentDetail: 'payments/:paymentId',
CreatePayment: 'payments/new',
},
},
TransactionsTab: {
screens: {
TransactionList: 'transactions',
TransactionDetail: 'transactions/:transactionId',
},
},
},
},
},
},
};

// Usage in NavigationContainer
<NavigationContainer linking={linking}>
{/* Navigator */}
</NavigationContainer>

// hooks/useNavigationPersistence.ts
import { useRef, useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import type { NavigationState } from '@react-navigation/native';

const NAVIGATION_STATE_KEY = 'NAVIGATION_STATE';

export function useNavigationPersistence() {
const [isReady, setIsReady] = React.useState(false);
const [initialState, setInitialState] = React.useState<NavigationState | undefined>();

useEffect(() => {
const restoreState = async () => {
try {
const savedStateString = await AsyncStorage.getItem(NAVIGATION_STATE_KEY);
const state = savedStateString ? JSON.parse(savedStateString) : undefined;
setInitialState(state);
} finally {
setIsReady(true);
}
};

restoreState();
}, []);

const onStateChange = async (state: NavigationState | undefined) => {
if (state) {
await AsyncStorage.setItem(NAVIGATION_STATE_KEY, JSON.stringify(state));
}
};

return {
isReady,
initialState,
onStateChange,
};
}

// Usage in RootNavigator
export default function RootNavigator() {
const { isReady, initialState, onStateChange } = useNavigationPersistence();

if (!isReady) {
return <SplashScreen />;
}

return (
<NavigationContainer
initialState={initialState}
onStateChange={onStateChange}
>
{/* Navigator */}
</NavigationContainer>
);
}

Common Mistakes

Not Using Type-Safe Navigation

// BAD: No type safety
const handlePress = () => {
navigation.navigate('PaymentDetail', {
id: 123, // Wrong type! Should be string
});
};
// GOOD: Type-safe navigation
import type { PaymentDetailNavigationProp } from '../../types/navigation';

const navigation = useNavigation<PaymentDetailNavigationProp>();

const handlePress = () => {
navigation.navigate('PaymentDetail', {
paymentId: '123', // Correct type
});
};

Mixing Navigation and Business Logic

// BAD: Payment logic in navigation handler
const handlePayment = async () => {
const result = await createPayment(amount);
if (result.success) {
navigation.navigate('PaymentSuccess');
}
};
// GOOD: Separate concerns
const { mutate: createPayment } = useCreatePayment({
onSuccess: () => {
navigation.navigate('PaymentSuccess');
},
});

const handlePayment = () => {
createPayment({ amount });
};

Code Review Checklist

Type Safety

  • All route param lists defined with TypeScript
  • Navigation props typed in screen components
  • Route props typed for param access
  • No 'any' types in navigation code
  • TypeScript strict mode catches navigation errors
  • Authentication-aware navigation (separate auth/app trees)
  • Proper nesting (tabs contain stacks, not vice versa)
  • Modal presentation used for create/edit flows
  • Screen options configured (titles, headers, gestures)
  • Tab icons use outline/filled variants based on focus

Deep Linking

  • Linking config complete for all deep-linkable screens
  • URL prefixes configured (custom scheme + https)
  • Parameter extraction works for all routes
  • Cold start handling tested
  • Notification deep links tested

Performance

  • Lazy loading for heavy screens
  • Screen options memoized to prevent re-renders
  • Navigation persistence only for relevant state
  • Tab pre-rendering avoided (use lazy mounting)
  • Large lists use FlatList in scrollable screens

Banking-Specific

  • Biometric re-auth on app resume from background
  • Sensitive screens (payment confirm) prevent screenshots
  • Back navigation blocked on critical confirmation screens
  • Navigation state doesn't leak sensitive data
  • Deep links validated for security (no arbitrary navigation)

Further Reading

React Native Framework Guidelines

Cross-Platform Guidelines

External Resources


Summary

Key Takeaways

  1. Type-safe navigation - Define all route parameters with TypeScript
  2. Authentication-aware - Separate navigation trees for auth/app states
  3. Stack inside tabs - Proper nesting hierarchy
  4. Deep linking - Support payment confirmations from notifications
  5. Modal presentation - Use for create/edit flows
  6. State persistence - Save navigation state for offline scenarios
  7. Performance - Lazy load screens, optimize transitions
  8. Security - Biometric re-auth, prevent screenshots on sensitive screens
  9. No business logic - Navigation only handles routing
  10. Accessibility - Screen reader support for route changes

Next Steps: Review React Native UI for component architecture and styling patterns, then explore React Native Data for API integration and offline support.