reactnative
/

React Native Stack Navigator – Complete Guide to Native Stack Navigation

Last Sync: Today

On this page

12
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

reactnative

React Native Stack Navigator – Complete Guide to Native Stack Navigation

Stack Navigator – The Foundation of Mobile Navigation

Stack Navigator is the most common navigation pattern in mobile apps. It manages a stack of screens, pushing new screens onto the stack and popping to go back. React Navigation provides two stack implementations: createNativeStackNavigator (recommended, uses native UINavigationController on iOS and Fragment on Android) and createStackNavigator (JavaScript-based, for custom transitions). In 2026, NativeStackNavigator is the default choice for production apps.

  1. Installation & Setup

Native Stack Navigator requires the core navigation package and native dependencies. For Expo managed projects, use npx expo install. For bare React Native projects, manual linking may be required.

BASHRead-only
1
# Core packages
npm install @react-navigation/native @react-navigation/native-stack

# Peer dependencies (Expo managed)
npx expo install react-native-screens react-native-safe-area-context

# Peer dependencies (bare React Native)
npm install react-native-screens react-native-safe-area-context
cd ios && pod install && cd ..

# For Android, add to MainActivity.java:
# import android.os.Bundle;
# @Override
# protected void onCreate(Bundle savedInstanceState) {
#   super.onCreate(null);
# }

  1. Basic Native Stack Navigator

Create a stack navigator with screens, configure options, and navigate between them using navigation methods.

React TSXRead-only
1
// App.tsx - Basic Native Stack Navigator
import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { Button, View, Text, StyleSheet, Alert } from 'react-native';

// Type definitions for navigation params
type RootStackParamList = {
  Home: undefined;
  Details: { itemId: number; title: string };
  Profile: { userId: string };
  Modal: { message: string };
};

const Stack = createNativeStackNavigator<RootStackParamList>();

// Home Screen
function HomeScreen({ navigation }: { navigation: any }) {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Home Screen</Text>
      
      {/* Basic navigation with params */}
      <Button
        title="Go to Details"
        onPress={() => navigation.navigate('Details', { 
          itemId: 42, 
          title: 'The Answer' 
        })}
      />
      
      {/* Push always adds new screen */}
      <Button
        title="Push Details (always new)"
        onPress={() => navigation.push('Details', { 
          itemId: Date.now(), 
          title: 'New Instance' 
        })}
      />
      
      {/* Replace current screen */}
      <Button
        title="Replace with Profile"
        onPress={() => navigation.replace('Profile', { userId: 'new_user' })}
      />
      
      {/* Go back */}
      <Button
        title="Go Back"
        onPress={() => navigation.goBack()}
      />
      
      {/* Pop to first screen */}
      <Button
        title="Pop to Top"
        onPress={() => navigation.popToTop()}
      />
    </View>
  );
}

// Details Screen with params
function DetailsScreen({ route, navigation }: { route: any; navigation: any }) {
  const { itemId, title } = route.params;
  
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Details Screen</Text>
      <Text>Item ID: {itemId}</Text>
      <Text>Title: {title}</Text>
      
      {/* Update current screen params */}
      <Button
        title="Update Title"
        onPress={() => navigation.setParams({ title: 'Updated Title' })}
      />
      
      {/* Navigate to parent */}
      <Button
        title="Go to Home"
        onPress={() => navigation.popToTop()}
      />
    </View>
  );
}

// Profile Screen
function ProfileScreen({ route, navigation }: { route: any; navigation: any }) {
  const { userId } = route.params;
  
  // Navigate and replace current screen
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Profile</Text>
      <Text>User ID: {userId}</Text>
      <Button title="Edit Profile" onPress={() => navigation.navigate('Modal', { 
        message: 'Edit your profile' 
      })} />
    </View>
  );
}

// Modal Screen (different presentation)
function ModalScreen({ route, navigation }: { route: any; navigation: any }) {
  return (
    <View style={styles.modalContainer}>
      <Text style={styles.title}>Modal</Text>
      <Text>{route.params.message}</Text>
      <Button title="Close" onPress={() => navigation.goBack()} />
    </View>
  );
}

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator
        initialRouteName="Home"
        screenOptions={{
          headerStyle: { backgroundColor: '#007AFF' },
          headerTintColor: '#FFFFFF',
          headerTitleStyle: { fontWeight: 'bold' },
          headerBackTitle: 'Back',
          gestureEnabled: true, // iOS swipe back
          animation: 'slide_from_right', // default animation
        }}
      >
        <Stack.Screen
          name="Home"
          component={HomeScreen}
          options={{ title: 'Welcome' }}
        />
        <Stack.Screen
          name="Details"
          component={DetailsScreen}
          options={({ route }) => ({ 
            title: route.params.title 
          })}
        />
        <Stack.Screen
          name="Profile"
          component={ProfileScreen}
          options={{ title: 'User Profile' }}
        />
        {/* Modal presentation */}
        <Stack.Screen
          name="Modal"
          component={ModalScreen}
          options={{
            presentation: 'modal',
            headerShown: false,
          }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    gap: 16,
    padding: 16,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 20,
  },
  modalContainer: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: 'rgba(0,0,0,0.9)',
    gap: 16,
  },
});

// Navigation Methods Reference:
// navigation.navigate('RouteName', params) - Navigate to route, don't push if exists
// navigation.push('RouteName', params) - Always push new screen
// navigation.goBack() - Go back one screen
// navigation.popToTop() - Go back to first screen in stack
// navigation.replace('RouteName', params) - Replace current screen
// navigation.setParams(params) - Update current screen params
// navigation.canGoBack() - Check if can go back

  1. Header Customization

Native Stack Navigator provides extensive header customization options including custom components, buttons, large titles, and search bars.

React TSXRead-only
1
// HeaderCustomization.tsx
import * as React from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { Button, View, Text, TouchableOpacity, Platform } from 'react-native';
import { Ionicons } from '@expo/vector-icons';

const Stack = createNativeStackNavigator();

function HomeScreen({ navigation }: { navigation: any }) {
  React.useLayoutEffect(() => {
    navigation.setOptions({
      headerRight: () => (
        <Button
          onPress={() => navigation.navigate('Settings')}
          title="Settings"
          color="#fff"
        />
      ),
      headerLeft: () => (
        <TouchableOpacity onPress={() => console.log('Menu pressed')} style={{ marginLeft: 16 }}>
          <Ionicons name="menu" size={24} color="#fff" />
        </TouchableOpacity>
      ),
    });
  }, [navigation]);

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Home Screen</Text>
    </View>
  );
}

function SettingsScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Settings Screen</Text>
    </View>
  );
}

export default function HeaderCustomizationApp() {
  return (
    <NavigationContainer>
      <Stack.Navigator
        screenOptions={{
          headerStyle: { backgroundColor: '#007AFF' },
          headerTintColor: '#fff',
          headerTitleStyle: { fontWeight: 'bold', fontSize: 20 },
          // iOS large title
          headerLargeTitle: Platform.OS === 'ios',
          headerLargeTitleStyle: { fontSize: 34, fontWeight: 'bold' },
          // Header shadow
          headerShadowVisible: false,
          // Back button customization
          headerBackTitle: 'Back',
          headerBackTitleStyle: { fontSize: 16 },
          headerBackVisible: true,
          // Hide header for specific screens
          // headerShown: false,
        }}
      >
        <Stack.Screen
          name="Home"
          component={HomeScreen}
          options={{
            title: 'Dashboard',
            // Large title only on this screen
            headerLargeTitle: true,
          }}
        />
        <Stack.Screen
          name="Settings"
          component={SettingsScreen}
          options={{
            title: 'App Settings',
            presentation: 'card',
          }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

// Custom header component
function CustomHeader() {
  return (
    <View style={{ flexDirection: 'row', alignItems: 'center', padding: 16 }}>
      <Text style={{ fontSize: 20, fontWeight: 'bold' }}>Custom Header</Text>
    </View>
  );
}

// Usage in screen options:
// header: (props) => <CustomHeader {...props} />
// headerTitle: (props) => <CustomTitle {...props} />
// headerBackground: () => <LinearGradient colors={['#007AFF', '#0055CC']} />

  1. Screen Transitions & Animations

Native Stack Navigator supports various screen transition animations including default, fade, flip, and custom animations on iOS.

React TSXRead-only
1
// ScreenTransitions.tsx
import * as React from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { Button, View, Text, StyleSheet } from 'react-native';

const Stack = createNativeStackNavigator();

function HomeScreen({ navigation }: { navigation: any }) {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Home</Text>
      <Button title="Slide from Right" onPress={() => navigation.navigate('SlideScreen')} />
      <Button title="Fade Animation" onPress={() => navigation.navigate('FadeScreen')} />
      <Button title="Flip Animation" onPress={() => navigation.navigate('FlipScreen')} />
      <Button title="Modal Presentation" onPress={() => navigation.navigate('ModalScreen')} />
    </View>
  );
}

function SlideScreen() {
  return (
    <View style={styles.container}>
      <Text>Slide from Right (Default)</Text>
    </View>
  );
}

function FadeScreen() {
  return (
    <View style={styles.container}>
      <Text>Fade Animation</Text>
    </View>
  );
}

function FlipScreen() {
  return (
    <View style={styles.container}>
      <Text>Flip Horizontal (iOS only)</Text>
    </View>
  );
}

function ModalContentScreen() {
  return (
    <View style={styles.modalContainer}>
      <Text>Modal Presentation</Text>
    </View>
  );
}

export default function TransitionsApp() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen name="Home" component={HomeScreen} />
        
        {/* Different animations */}
        <Stack.Screen
          name="SlideScreen"
          component={SlideScreen}
          options={{ animation: 'slide_from_right' }}
        />
        <Stack.Screen
          name="FadeScreen"
          component={FadeScreen}
          options={{ animation: 'fade' }}
        />
        <Stack.Screen
          name="FlipScreen"
          component={FlipScreen}
          options={{ animation: 'flip' }}
        />
        
        {/* Presentation styles */}
        <Stack.Screen
          name="ModalScreen"
          component={ModalContentScreen}
          options={{
            presentation: 'modal',
            animation: 'slide_from_bottom',
            headerShown: false,
          }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 },
  title: { fontSize: 24, fontWeight: 'bold' },
  modalContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(0,0,0,0.9)' },
});

// Animation Options:
// animation: 'slide_from_right' (default)
// animation: 'slide_from_left'
// animation: 'slide_from_bottom'
// animation: 'fade'
// animation: 'flip' (iOS only)
// animation: 'none'
//
// Presentation Options:
// presentation: 'card' (default)
// presentation: 'modal' (iOS modal style, slide from bottom)
// presentation: 'transparentModal' (modal with transparent background)
// presentation: 'fullScreenModal' (full screen modal)
// presentation: 'formSheet' (iOS form sheet on iPad)

  1. Navigation Lifecycle & Hooks

React Navigation provides hooks to respond to screen focus, blur, and state changes. Use these for data fetching, analytics, and cleanup.

React TSXRead-only
1
// NavigationLifecycle.tsx
import * as React from 'react';
import {
  useFocusEffect,
  useIsFocused,
  useNavigation,
  useRoute,
} from '@react-navigation/native';
import { View, Text, Button, ActivityIndicator } from 'react-native';

function DataFetchingScreen() {
  const [isLoading, setIsLoading] = React.useState(false);
  const [data, setData] = React.useState(null);

  // Method 1: useFocusEffect (recommended for data fetching)
  useFocusEffect(
    React.useCallback(() => {
      let isActive = true;
      
      const fetchData = async () => {
        setIsLoading(true);
        try {
          const result = await fetch('https://api.example.com/data');
          const json = await result.json();
          if (isActive) {
            setData(json);
          }
        } catch (error) {
          console.error(error);
        } finally {
          if (isActive) {
            setIsLoading(false);
          }
        }
      };

      fetchData();

      // Cleanup when screen loses focus
      return () => {
        isActive = false;
        console.log('Screen blurred - cleanup');
      };
    }, [])
  );

  // Method 2: useIsFocused (triggers re-render)
  const isFocused = useIsFocused();

  if (isLoading) {
    return <ActivityIndicator size="large" />;
  }

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Screen Focused: {isFocused ? 'Yes' : 'No'}</Text>
      <Text>{JSON.stringify(data)}</Text>
    </View>
  );
}

// BeforeRemove event (prevent accidental navigation)
function FormScreen({ navigation }: { navigation: any }) {
  const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(false);

  React.useEffect(() => {
    const unsubscribe = navigation.addListener('beforeRemove', (e: any) => {
      if (!hasUnsavedChanges) {
        return;
      }

      e.preventDefault();
      
      // Show confirmation dialog
      Alert.alert(
        'Discard changes?',
        'You have unsaved changes. Are you sure you want to leave?',
        [
          { text: 'Stay', style: 'cancel', onPress: () => {} },
          {
            text: 'Discard',
            style: 'destructive',
            onPress: () => navigation.dispatch(e.data.action),
          },
        ]
      );
    });

    return unsubscribe;
  }, [navigation, hasUnsavedChanges]);

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Form Screen</Text>
      <Button title="Edit" onPress={() => setHasUnsavedChanges(true)} />
    </View>
  );
}

// Tab press to scroll to top
function ScrollableScreen() {
  const scrollRef = React.useRef(null);
  const navigation = useNavigation();

  React.useEffect(() => {
    const unsubscribe = navigation.addListener('tabPress', (e: any) => {
      // Prevent default tab behavior
      // e.preventDefault();
      
      // Scroll to top
      scrollRef.current?.scrollTo({ y: 0, animated: true });
    });

    return unsubscribe;
  }, [navigation]);

  return (
    <ScrollView ref={scrollRef}>
      {[...Array(50)].map((_, i) => (
        <Text key={i}>Item {i + 1}</Text>
      ))}
    </ScrollView>
  );
}

// Navigation state tracking
function NavigationTracker() {
  const navigation = useNavigation();
  const route = useRoute();

  React.useEffect(() => {
    // Track screen view for analytics
    console.log(`Screen viewed: ${route.name}`);
    
    // Track navigation state changes
    const unsubscribe = navigation.addListener('state', (e) => {
      console.log('Navigation state changed', e.data.state);
    });

    return unsubscribe;
  }, [navigation, route]);

  return null;
}

// Lifecycle Events:
// focus - Screen came into focus
// blur - Screen went out of focus
// beforeRemove - Screen is about to be removed
// state - Navigation state changed
// transitionStart - Transition animation started
// transitionEnd - Transition animation ended

  1. Passing & Accessing Parameters

Pass data between screens using route params. TypeScript ensures type safety for parameters.

React TSXRead-only
1
// ParamsExample.tsx - Full TypeScript implementation
import * as React from 'react';
import {
  NavigationContainer,
  RouteProp,
  useNavigation,
  useRoute,
} from '@react-navigation/native';
import { createNativeStackNavigator, NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Button, View, Text, TextInput, StyleSheet } from 'react-native';

// Define param types for each screen
type RootStackParamList = {
  Home: undefined;
  Product: { id: number; name: string };
  Checkout: {
    items: Array<{ id: number; name: string; price: number }>;
    total: number;
    promoCode?: string;
  };
  UserProfile: { userId: string; readonly?: boolean };
};

// Type-safe navigation and route hooks
type HomeScreenNavigationProp = NativeStackNavigationProp<RootStackParamList, 'Home'>;
type ProductScreenRouteProp = RouteProp<RootStackParamList, 'Product'>;
type CheckoutScreenRouteProp = RouteProp<RootStackParamList, 'Checkout'>;

const Stack = createNativeStackNavigator<RootStackParamList>();

function HomeScreen() {
  const navigation = useNavigation<HomeScreenNavigationProp>();

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Products</Text>
      
      {/* Passing single object */}
      <Button
        title="View Product 1"
        onPress={() => navigation.navigate('Product', { id: 1, name: 'iPhone 15 Pro' })}
      />
      
      {/* Passing complex data */}
      <Button
        title="Go to Checkout"
        onPress={() =>
          navigation.navigate('Checkout', {
            items: [
              { id: 1, name: 'iPhone', price: 999 },
              { id: 2, name: 'Case', price: 49 },
            ],
            total: 1048,
            promoCode: 'SAVE10',
          })
        }
      />
    </View>
  );
}

function ProductScreen() {
  const route = useRoute<ProductScreenRouteProp>();
  const navigation = useNavigation();
  const { id, name } = route.params;

  // Update params
  const updateProduct = () => {
    navigation.setParams({ name: `${name} (Updated)` });
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>{name}</Text>
      <Text>Product ID: {id}</Text>
      <Button title="Update Name" onPress={updateProduct} />
    </View>
  );
}

function CheckoutScreen() {
  const route = useRoute<CheckoutScreenRouteProp>();
  const { items, total, promoCode } = route.params;

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Checkout</Text>
      <Text>Items: {items.length}</Text>
      <Text>Total: ${total}</Text>
      {promoCode && <Text>Promo Code: {promoCode}</Text>}
    </View>
  );
}

// Default params (when param is optional)
function UserProfileScreen() {
  const route = useRoute<RouteProp<RootStackParamList, 'UserProfile'>>();
  const { userId, readonly = false } = route.params;

  return (
    <View style={styles.container}>
      <Text>User: {userId}</Text>
      <Text>Readonly: {readonly ? 'Yes' : 'No'}</Text>
    </View>
  );
}

export default function ParamsApp() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Product" component={ProductScreen} />
        <Stack.Screen name="Checkout" component={CheckoutScreen} />
        <Stack.Screen name="UserProfile" component={UserProfileScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 },
  title: { fontSize: 24, fontWeight: 'bold' },
});

// Parameter best practices:
// 1. Only pass serializable data (avoid functions, class instances)
// 2. Use TypeScript for type safety
// 3. Keep params minimal - pass IDs, fetch full data in child screen
// 4. Use optional params with default values
// 5. Avoid passing entire objects when ID would suffice
// 6. Use route.params?.field for optional params

  1. Nesting Stack Navigators

Nesting stack navigators inside tabs or drawers is common. Access parent navigators and pass params between nested stacks.

React TSXRead-only
1
// NestedStacks.tsx - Stack inside Tab Navigator
import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Button, View, Text } from 'react-native';
import { Ionicons } from '@expo/vector-icons';

// ----- Home Stack -----
type HomeStackParamList = {
  HomeList: undefined;
  HomeDetail: { id: number };
};

const HomeStack = createNativeStackNavigator<HomeStackParamList>();

function HomeListScreen({ navigation }: { navigation: any }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 }}>
      <Text>Home List</Text>
      <Button
        title="Go to Detail"
        onPress={() => navigation.navigate('HomeDetail', { id: 42 })}
      />
      {/* Access parent tab navigator */}
      <Button
        title="Open Drawer (if exists)"
        onPress={() => navigation.getParent()?.openDrawer?.()}
      />
    </View>
  );
}

function HomeDetailScreen({ route, navigation }: { route: any; navigation: any }) {
  const { id } = route.params;
  
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 }}>
      <Text>Home Detail - ID: {id}</Text>
      <Button title="Go Back" onPress={() => navigation.goBack()} />
      {/* Navigate in parent tab */}
      <Button
        title="Go to Profile Tab"
        onPress={() => navigation.getParent()?.navigate('ProfileTab')}
      />
    </View>
  );
}

function HomeStackScreen() {
  return (
    <HomeStack.Navigator>
      <HomeStack.Screen name="HomeList" component={HomeListScreen} />
      <HomeStack.Screen name="HomeDetail" component={HomeDetailScreen} />
    </HomeStack.Navigator>
  );
}

// ----- Profile Stack -----
type ProfileStackParamList = {
  ProfileMain: undefined;
  ProfileEdit: undefined;
};

const ProfileStack = createNativeStackNavigator<ProfileStackParamList>();

function ProfileMainScreen({ navigation }: { navigation: any }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 }}>
      <Text>Profile Main</Text>
      <Button title="Edit Profile" onPress={() => navigation.navigate('ProfileEdit')} />
    </View>
  );
}

function ProfileEditScreen() {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Edit Profile</Text>
    </View>
  );
}

function ProfileStackScreen() {
  return (
    <ProfileStack.Navigator>
      <ProfileStack.Screen name="ProfileMain" component={ProfileMainScreen} />
      <ProfileStack.Screen name="ProfileEdit" component={ProfileEditScreen} />
    </ProfileStack.Navigator>
  );
}

// ----- Tab Navigator -----
type TabParamList = {
  HomeTab: undefined;
  ProfileTab: undefined;
};

const Tab = createBottomTabNavigator<TabParamList>();

function MainTabNavigator() {
  return (
    <Tab.Navigator
      screenOptions={({ route }) => ({
        tabBarIcon: ({ focused, color, size }) => {
          const iconName = route.name === 'HomeTab' ? 'home' : 'person';
          return <Ionicons name={focused ? iconName : `${iconName}-outline`} size={size} color={color} />;
        },
        tabBarActiveTintColor: '#007AFF',
        headerShown: false,
      })}
    >
      <Tab.Screen name="HomeTab" component={HomeStackScreen} />
      <Tab.Screen name="ProfileTab" component={ProfileStackScreen} />
    </Tab.Navigator>
  );
}

export default function NestedStacksApp() {
  return (
    <NavigationContainer>
      <MainTabNavigator />
    </NavigationContainer>
  );
}

// Accessing parent navigators:
// navigation.getParent() - Returns parent navigator
// navigation.getParent('TabNavigator') - Get specific parent by ID
// navigation.dispatch(CommonActions.navigate({ name: 'Tab', params: {} }))

// Passing params between nested stacks:
// Option 1: Use navigation.getParent()?.navigate('Tab', { screen: 'Screen', params: {} })
// Option 2: Use navigation.dispatch with CommonActions
// Option 3: Use global state (Redux, Zustand) for shared data

  1. Deep Linking with Stack Navigator

Configure deep linking to open specific screens from URLs. React Navigation 8 supports Zod schemas for parameter validation.

React TSXRead-only
1
// DeepLinkingConfig.tsx
import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { View, Text, Linking } from 'react-native';
import { z } from 'zod'; // React Navigation 8 supports Zod

const Stack = createNativeStackNavigator();

// Deep linking configuration
const linking = {
  prefixes: ['myapp://', 'https://myapp.com', 'https://www.myapp.com'],
  
  // React Navigation 8 - Automatic path generation
  // Paths are automatically generated from screen names (PascalCase -> kebab-case)
  // Example: ProductDetailScreen -> /product-detail-screen
  
  // Custom path configuration
  config: {
    screens: {
      Home: {
        path: '', // root
        exact: true,
      },
      ProductList: 'products',
      ProductDetail: {
        path: 'product/:id',
        parse: {
          // React Navigation 8 - Zod schema for validation
          id: z.coerce.number(), // Convert string to number, reject if not a number
        },
        stringify: {
          id: (id: number) => id.toString(),
        },
      },
      Profile: 'user/:userId',
      Settings: 'settings',
      // Nested navigators
      Modal: {
        path: 'modal',
        screens: {
          ModalContent: 'content',
        },
      },
    },
  },
  
  // Optional: Get initial URL from the app
  async getInitialURL() {
    const url = await Linking.getInitialURL();
    return url;
  },
  
  // Optional: Subscribe to URL events
  subscribe(listener: (url: string) => void) {
    const onReceiveURL = ({ url }: { url: string }) => listener(url);
    Linking.addEventListener('url', onReceiveURL);
    return () => Linking.removeEventListener('url', onReceiveURL);
  },
};

function HomeScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Home Screen</Text>
    </View>
  );
}

function ProductListScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Product List</Text>
    </View>
  );
}

function ProductDetailScreen({ route }: { route: any }) {
  const { id } = route.params;
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Product Detail - ID: {id}</Text>
    </View>
  );
}

function ProfileScreen({ route }: { route: any }) {
  const { userId } = route.params;
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Profile - User: {userId}</Text>
    </View>
  );
}

function SettingsScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Settings</Text>
    </View>
  );
}

// React Navigation 8 - Standard Schema support
// You can use Zod, Valibot, or ArkType for param validation
// Benefits:
// - Validation: If validation fails, URL won't match the screen
// - Fallback: Schemas called with undefined when param missing
// - Better TypeScript inference for required/optional params

// For React Navigation 7, use parse function:
// parse: {
//   id: (id: string) => Number(id),
// }

export default function DeepLinkingApp() {
  return (
    <NavigationContainer linking={linking}>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="ProductList" component={ProductListScreen} />
        <Stack.Screen name="ProductDetail" component={ProductDetailScreen} />
        <Stack.Screen name="Profile" component={ProfileScreen} />
        <Stack.Screen name="Settings" component={SettingsScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

// URL Examples:
// myapp://
// myapp://products
// myapp://product/42
// myapp://user/johndoe
// https://myapp.com/product/100
// https://www.myapp.com/settings

// Testing deep links (iOS Simulator):
// xcrun simctl openurl booted myapp://product/123

// Testing deep links (Android Emulator):
// adb shell am start -W -a android.intent.action.VIEW -d "myapp://product/123" com.myapp

  1. TypeScript Best Practices

Full TypeScript integration ensures type safety for navigation and route parameters.

React TSXRead-only
1
// types/navigation.ts - Global navigation types
export type RootStackParamList = {
  Home: undefined;
  Details: { id: number; title: string };
  Profile: { userId: string; readonly?: boolean };
  Modal: undefined;
};

// Global type declaration
declare global {
  namespace ReactNavigation {
    interface RootParamList extends RootStackParamList {}
  }
}

// Screen component with full typing
import type { NativeStackScreenProps } from '@react-navigation/native-stack';

type DetailsScreenProps = NativeStackScreenProps<RootStackParamList, 'Details'>;

function DetailsScreen({ route, navigation }: DetailsScreenProps) {
  const { id, title } = route.params;
  // TypeScript knows id is number, title is string
  
  return (
    <View>
      <Text>{title}</Text>
      <Button title="Go Back" onPress={() => navigation.goBack()} />
    </View>
  );
}

// useNavigation with type parameter
type HomeScreenNavigationProp = NativeStackNavigationProp<RootStackParamList, 'Home'>;

function HomeScreen() {
  const navigation = useNavigation<HomeScreenNavigationProp>();
  
  // Type-safe navigate call
  navigation.navigate('Details', { id: 42, title: 'Hello' });
  // Error: navigation.navigate('Details', { wrong: 'param' });
  
  return null;
}

// React Navigation 8 - Automatic type inference with createXScreen
import { createNativeStackScreen } from '@react-navigation/native-stack';

const Stack = createStackNavigator({
  screens: {
    Profile: createNativeStackScreen({
      screen: ProfileScreen,
      linking: {
        path: 'user/:userId',
        parse: {
          userId: (userId) => Number(userId),
        },
      },
      options: ({ route }) => {
        // route.params.userId is automatically typed as number
        return { title: `User ${route.params.userId}` };
      },
    }),
  },
});

// useRoute with type parameter
const route = useRoute<RouteProp<RootStackParamList, 'Details'>>();
const { id, title } = route.params;

// Optional params with default values
type UserProfileParams = {
  userId: string;
  readonly?: boolean;
};

function UserProfileScreen({ route }: { route: RouteProp<RootStackParamList, 'Profile'> }) {
  const { userId, readonly = false } = route.params;
  return <Text>{userId} - Readonly: {readonly}</Text>;
}

// Navigation service with TypeScript
import { createNavigationContainerRef } from '@react-navigation/native';

export const navigationRef = createNavigationContainerRef<RootStackParamList>();

export function navigate(name: keyof RootStackParamList, params?: any) {
  if (navigationRef.isReady()) {
    navigationRef.navigate(name, params);
  }
}

// Usage in non-component
// import { navigate } from './NavigationService';
// navigate('Details', { id: 1, title: 'Test' });

  1. Performance Optimization

Optimize stack navigator performance for large apps with lazy loading, screen freezing, and proper memoization.

React TSXRead-only
1
// PerformanceOptimizations.tsx
import * as React from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { View, Text, ActivityIndicator } from 'react-native';

const Stack = createNativeStackNavigator();

// 1. Lazy loading screens with React.lazy
const LazyProfileScreen = React.lazy(() => import('./ProfileScreen'));

function LazyLoadedScreen() {
  return (
    <React.Suspense fallback={<ActivityIndicator size="large" />}>
      <LazyProfileScreen />
    </React.Suspense>
  );
}

// 2. Memoize screen components
const MemoizedHomeScreen = React.memo(function HomeScreen() {
  return (
    <View>
      <Text>Memoized Home Screen</Text>
    </View>
  );
});

// 3. Freeze inactive screens (React Navigation 8)
// Prevents rendering of inactive screens, improves performance
function FreezingExample() {
  return (
    <Stack.Navigator
      screenOptions={{
        freezeOnBlur: true, // React Navigation 8 - freeze inactive screens
        // inactiveBehavior: 'pause', // Clean up effects on inactive screens
      }}
    >
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen name="Details" component={DetailsScreen} />
    </Stack.Navigator>
  );
}

// 4. Detach inactive screens (Native Stack only)
// Completely detaches screens from view hierarchy when inactive
function DetachInactiveExample() {
  return (
    <Stack.Navigator
      screenOptions={{
        detachInactiveScreens: true, // Native Stack only, improves memory
      }}
    >
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen name="Details" component={DetailsScreen} />
    </Stack.Navigator>
  );
}

// 5. Avoid heavy components in header
// Bad:
// options={{
//   headerRight: () => <ExpensiveComponent /> // Re-renders frequently
// }}

// Good:
// const MemoizedHeaderRight = React.memo(() => <SimpleButton />);
// options={{ headerRight: () => <MemoizedHeaderRight /> }}

// 6. Use useCallback for navigation handlers
function HomeScreenWithHandlers() {
  const navigation = useNavigation();
  
  const handlePress = React.useCallback(() => {
    navigation.navigate('Details');
  }, [navigation]);
  
  return <Button title="Go" onPress={handlePress} />;
}

// 7. Avoid inline functions in options
// Bad:
// <Stack.Screen
//   options={{
//     headerRight: () => <Button onPress={() => console.log('press')} /> // New function each render
//   }}
// />

// Good:
// const HeaderRight = React.memo(() => <Button onPress={handlePress} />);
// <Stack.Screen options={{ headerRight: HeaderRight }} />

// 8. Use getFocusedRouteNameFromRoute to conditionally render tab bars
import { getFocusedRouteNameFromRoute } from '@react-navigation/native';

function getTabBarVisibility(route: any) {
  const routeName = getFocusedRouteNameFromRoute(route) ?? 'Home';
  const hideOnScreens = ['Details', 'Profile'];
  return { display: hideOnScreens.includes(routeName) ? 'none' : 'flex' };
}

// Performance Checklist:
// ✅ Use freezeOnBlur: true (React Navigation 8)
// ✅ Use detachInactiveScreens: true (Native Stack)
// ✅ Lazy load heavy screens
// ✅ Memoize screen components
// ✅ Use useCallback for navigation handlers
// ✅ Avoid inline functions in options
// ✅ Keep header components simple and memoized
// ✅ Profile with React DevTools and Flipper

  1. Common Patterns & Best Practices

Production-proven patterns for stack navigator implementation.

React TSXRead-only
1
// NavigationService.ts - Navigation outside components
import { createNavigationContainerRef, CommonActions } from '@react-navigation/native';
import { RootStackParamList } from './types';

export const navigationRef = createNavigationContainerRef<RootStackParamList>();

export function navigate(name: keyof RootStackParamList, params?: any) {
  if (navigationRef.isReady()) {
    navigationRef.navigate(name, params);
  }
}

export function goBack() {
  if (navigationRef.isReady() && navigationRef.canGoBack()) {
    navigationRef.goBack();
  }
}

export function resetAndNavigate(name: keyof RootStackParamList) {
  if (navigationRef.isReady()) {
    navigationRef.dispatch(
      CommonActions.reset({
        index: 0,
        routes: [{ name }],
      })
    );
  }
}

// useBeforeRemove.ts - Prevent accidental navigation
import { useBeforeRemove } from '@react-navigation/native';

export function usePreventNavigation(hasUnsavedChanges: boolean) {
  const navigation = useNavigation();
  
  useBeforeRemove((e) => {
    if (!hasUnsavedChanges) return;
    
    e.preventDefault();
    
    Alert.alert(
      'Discard changes?',
      'You have unsaved changes. Are you sure?',
      [
        { text: 'Stay', style: 'cancel' },
        {
          text: 'Discard',
          style: 'destructive',
          onPress: () => navigation.dispatch(e.data.action),
        },
      ]
    );
  });
}

// useFocusRefresh.ts - Refresh data on focus
import { useFocusEffect } from '@react-navigation/native';

export function useFocusRefresh(refreshFn: () => Promise<void>) {
  useFocusEffect(
    React.useCallback(() => {
      refreshFn();
      return () => {};
    }, [refreshFn])
  );
}

// App.tsx - Complete setup with TypeScript
import { NavigationContainer } from '@react-navigation/native';
import { navigationRef } from './NavigationService';

export default function App() {
  return (
    <NavigationContainer ref={navigationRef}>
      <RootNavigator />
    </NavigationContainer>
  );
}

// Best Practices Summary:
// 1. Use createNativeStackNavigator over createStackNavigator
// 2. Define TypeScript param types for all screens
// 3. Use useFocusEffect for data fetching on screen focus
// 4. Use navigation.push() when you need multiple instances of same screen
// 5. Use navigation.replace() for login/onboarding flows
// 6. Set freezeOnBlur: true for performance (React Navigation 8)
// 7. Use React.memo for screen components
// 8. Avoid passing non-serializable data in params
// 9. Use getFocusedRouteNameFromRoute to conditionally hide tab bars
// 10. Test deep links with simulators and real devices

Test Your Knowledge

Q1
of 4

Which navigation method always adds a new screen to the stack?

A
navigate
B
push
C
goBack
D
replace
Q2
of 4

What is the default animation for Native Stack Navigator?

A
fade
B
slide_from_bottom
C
slide_from_right
D
flip
Q3
of 4

Which hook should you use to fetch data when a screen becomes visible?

A
useEffect
B
useLayoutEffect
C
useFocusEffect
D
useIsFocused
Q4
of 4

Which React Navigation 8 feature improves performance by preventing inactive screen rendering?

A
detachInactiveScreens
B
freezeOnBlur
C
lazy
D
unmountOnBlur

Frequently Asked Questions

What's the difference between navigate and push?

navigate checks if a screen with that name already exists in the stack. If it does, it jumps to that existing screen. If not, it pushes a new screen. push always adds a new screen to the stack, even if an instance already exists. Use navigate for main navigation flows (prevents duplicate screens). Use push when you need multiple instances (e.g., product details from different categories).

How do I pass complex objects between screens?

Pass only serializable data (JSON-compatible). For large objects, pass an ID and fetch the full object in the child screen. For shared state, use global state management (Zustand, Redux). Avoid passing functions, class instances, or React components as params.

Why does my screen re-render when I navigate back?

When you navigate back, the previous screen becomes focused again, triggering a re-render. This is expected. If you're fetching data, use useFocusEffect with proper caching to avoid unnecessary network requests. For expensive re-renders, use React.memo on your screen components.

What's the difference between Native Stack and JS Stack?

Native Stack (createNativeStackNavigator) uses native navigation components (UINavigationController on iOS, Fragment on Android) for better performance, native gestures, and smaller bundle size. JS Stack (createStackNavigator) is JavaScript-based, supports custom transitions, but has lower performance. Use Native Stack for production apps.

Previous

react native navigation

Next

react native tab nav

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.