React Native Navigation: Mastering React Navigation v6+
Introduction
Navigation is the backbone of any mobile application, determining how users move between screens and interact with your app. React Navigation v6+ has revolutionized navigation in React Native with improved performance, better TypeScript support, and a more intuitive API.
This comprehensive guide covers everything from basic setup to advanced navigation patterns, helping you build seamless and performant navigation experiences in your React Native applications.
React Navigation v6 introduced significant changes including a new component-based API, improved type safety, and better performance optimizations that make it the go-to solution for React Native navigation.
React Navigation v6 Fundamentals
Installation and Setup
# Install required packages
npm install @react-navigation/native
# Install dependencies for React Native CLI
npm install react-native-screens react-native-safe-area-context
# For iOS, run pod install
cd ios && pod install
# Install navigation types
npm install @react-navigation/stack @react-navigation/bottom-tabs @react-navigation/drawer @react-navigation/native-stack
Basic Navigation Setup
// App.js - Basic navigation setup
import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { SafeAreaProvider } from "react-native-safe-area-context";
// Import your screens
import HomeScreen from "./screens/HomeScreen";
import ProfileScreen from "./screens/ProfileScreen";
import SettingsScreen from "./screens/SettingsScreen";
const Stack = createNativeStackNavigator();
export default function App() {
return (
<SafeAreaProvider>
<NavigationContainer>
<Stack.Navigator
initialRouteName="Home"
screenOptions={{
headerStyle: {
backgroundColor: "#6200ee",
},
headerTintColor: "#fff",
headerTitleStyle: {
fontWeight: "bold",
},
}}
>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: "Welcome Home" }}
/>
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={{ title: "User Profile" }}
/>
<Stack.Screen
name="Settings"
component={SettingsScreen}
options={{
title: "Settings",
headerRight: () => (
<TouchableOpacity onPress={() => console.log("Save pressed")}>
<Text style={{ color: "#fff", marginRight: 10 }}>Save</Text>
</TouchableOpacity>
),
}}
/>
</Stack.Navigator>
</NavigationContainer>
</SafeAreaProvider>
);
}
TypeScript Integration
// types/navigation.ts - Define navigation types
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
import type { BottomTabScreenProps } from "@react-navigation/bottom-tabs";
import type { CompositeScreenProps } from "@react-navigation/native";
// Define your stack parameter list
export type RootStackParamList = {
Home: undefined;
Profile: { userId: string; userName?: string };
Settings: undefined;
PostDetail: { postId: string; title: string };
EditProfile: { userId: string };
};
// Define tab navigator parameter list
export type TabParamList = {
HomeTab: undefined;
SearchTab: undefined;
ProfileTab: { userId?: string };
};
// Screen props types
export type HomeScreenProps = NativeStackScreenProps<
RootStackParamList,
"Home"
>;
export type ProfileScreenProps = NativeStackScreenProps<
RootStackParamList,
"Profile"
>;
export type PostDetailScreenProps = NativeStackScreenProps<
RootStackParamList,
"PostDetail"
>;
// Composite navigation props for nested navigators
export type HomeTabScreenProps = CompositeScreenProps<
BottomTabScreenProps<TabParamList, "HomeTab">,
NativeStackScreenProps<RootStackParamList>
>;
declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}
// screens/HomeScreen.tsx - TypeScript screen implementation
import React from "react";
import { View, Text, Button, StyleSheet } from "react-native";
import type { HomeScreenProps } from "../types/navigation";
export default function HomeScreen({ navigation, route }: HomeScreenProps) {
const navigateToProfile = () => {
navigation.navigate("Profile", {
userId: "12345",
userName: "John Doe",
});
};
const navigateToPostDetail = () => {
navigation.navigate("PostDetail", {
postId: "post-1",
title: "React Navigation Guide",
});
};
return (
<View style={styles.container}>
<Text style={styles.title}>Welcome to React Navigation v6!</Text>
<Button title="Go to Profile" onPress={navigateToProfile} />
<Button title="View Post Details" onPress={navigateToPostDetail} />
<Button
title="Open Settings"
onPress={() => navigation.navigate("Settings")}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 20,
},
title: {
fontSize: 24,
fontWeight: "bold",
marginBottom: 30,
textAlign: "center",
},
});
Navigation Patterns and Types
Stack Navigation
// Advanced Stack Navigator configuration
import React from "react";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { Platform } from "react-native";
const Stack = createNativeStackNavigator();
function AppStackNavigator() {
return (
<Stack.Navigator
initialRouteName="Home"
screenOptions={{
// Global screen options
headerStyle: {
backgroundColor: Platform.OS === "ios" ? "#f8f9fa" : "#6200ee",
},
headerTintColor: Platform.OS === "ios" ? "#6200ee" : "#fff",
headerTitleStyle: {
fontWeight: "600",
fontSize: 18,
},
// Enable gesture handling
gestureEnabled: true,
gestureDirection: "horizontal",
// Animation configuration
animation: "slide_from_right",
// Header configuration
headerBackTitleVisible: false,
headerShadowVisible: false,
}}
>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{
title: "Dashboard",
headerLargeTitle: true, // iOS only
}}
/>
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={({ route, navigation }) => ({
title: route.params?.userName || "Profile",
headerRight: () => (
<TouchableOpacity
onPress={() =>
navigation.navigate("EditProfile", {
userId: route.params.userId,
})
}
>
<Text style={{ color: "#6200ee" }}>Edit</Text>
</TouchableOpacity>
),
})}
/>
<Stack.Screen
name="PostDetail"
component={PostDetailScreen}
options={{
title: "Post Details",
presentation: "modal", // Present as modal
animation: "slide_from_bottom",
}}
/>
<Stack.Screen
name="FullScreenImage"
component={FullScreenImageScreen}
options={{
headerShown: false,
presentation: "fullScreenModal",
animation: "fade",
}}
/>
</Stack.Navigator>
);
}
Tab Navigation
// Bottom Tab Navigator with advanced configuration
import React from "react";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { Platform } from "react-native";
import Icon from "react-native-vector-icons/MaterialIcons";
const Tab = createBottomTabNavigator();
function MainTabNavigator() {
return (
<Tab.Navigator
initialRouteName="HomeTab"
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName;
switch (route.name) {
case "HomeTab":
iconName = "home";
break;
case "SearchTab":
iconName = "search";
break;
case "NotificationsTab":
iconName = "notifications";
break;
case "ProfileTab":
iconName = "person";
break;
default:
iconName = "help";
}
return <Icon name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: "#6200ee",
tabBarInactiveTintColor: "gray",
tabBarStyle: {
backgroundColor: "#fff",
borderTopWidth: 0,
elevation: 8,
shadowOpacity: 0.1,
shadowRadius: 4,
shadowColor: "#000",
shadowOffset: {
height: -2,
width: 0,
},
paddingBottom: Platform.OS === "ios" ? 20 : 5,
height: Platform.OS === "ios" ? 80 : 60,
},
tabBarLabelStyle: {
fontSize: 12,
fontWeight: "500",
},
// Header configuration
headerStyle: {
backgroundColor: "#6200ee",
},
headerTintColor: "#fff",
headerTitleStyle: {
fontWeight: "bold",
},
})}
>
<Tab.Screen
name="HomeTab"
component={HomeStackNavigator}
options={{
title: "Home",
tabBarBadge: 3, // Show badge
headerShown: false, // Hide header for nested stack
}}
/>
<Tab.Screen
name="SearchTab"
component={SearchScreen}
options={{
title: "Search",
tabBarIcon: ({ focused, color, size }) => (
<Icon
name="search"
size={size}
color={focused ? "#6200ee" : color}
/>
),
}}
/>
<Tab.Screen
name="NotificationsTab"
component={NotificationsScreen}
options={{
title: "Notifications",
tabBarBadge: 5,
tabBarBadgeStyle: {
backgroundColor: "#ff4444",
color: "#fff",
},
}}
/>
<Tab.Screen
name="ProfileTab"
component={ProfileScreen}
options={{
title: "Profile",
tabBarIcon: ({ focused, color, size }) => (
<View
style={{
borderRadius: size / 2,
borderWidth: focused ? 2 : 0,
borderColor: "#6200ee",
padding: focused ? 2 : 0,
}}
>
<Icon name="person" size={size} color={color} />
</View>
),
}}
/>
</Tab.Navigator>
);
}
Drawer Navigation
// Drawer Navigator with custom content
import React from "react";
import {
createDrawerNavigator,
DrawerContentScrollView,
DrawerItem,
} from "@react-navigation/drawer";
import { View, Text, StyleSheet, Image, TouchableOpacity } from "react-native";
import Icon from "react-native-vector-icons/MaterialIcons";
const Drawer = createDrawerNavigator();
// Custom Drawer Content Component
function CustomDrawerContent(props) {
const { navigation, state } = props;
return (
<DrawerContentScrollView {...props} style={styles.drawerContainer}>
{/* User Profile Section */}
<View style={styles.profileSection}>
<Image
source={{ uri: "https://via.placeholder.com/80" }}
style={styles.profileImage}
/>
<Text style={styles.userName}>John Doe</Text>
<Text style={styles.userEmail}>john.doe@example.com</Text>
</View>
{/* Navigation Items */}
<View style={styles.drawerItems}>
<DrawerItem
label="Dashboard"
onPress={() => navigation.navigate("Dashboard")}
icon={({ color, size }) => (
<Icon name="dashboard" color={color} size={size} />
)}
focused={state.index === 0}
activeTintColor="#6200ee"
inactiveTintColor="#666"
/>
<DrawerItem
label="Projects"
onPress={() => navigation.navigate("Projects")}
icon={({ color, size }) => (
<Icon name="folder" color={color} size={size} />
)}
focused={state.index === 1}
activeTintColor="#6200ee"
inactiveTintColor="#666"
/>
<DrawerItem
label="Settings"
onPress={() => navigation.navigate("Settings")}
icon={({ color, size }) => (
<Icon name="settings" color={color} size={size} />
)}
focused={state.index === 2}
activeTintColor="#6200ee"
inactiveTintColor="#666"
/>
<DrawerItem
label="Help & Support"
onPress={() => navigation.navigate("Help")}
icon={({ color, size }) => (
<Icon name="help" color={color} size={size} />
)}
activeTintColor="#6200ee"
inactiveTintColor="#666"
/>
</View>
{/* Logout Button */}
<View style={styles.logoutSection}>
<TouchableOpacity
style={styles.logoutButton}
onPress={() => {
// Handle logout
navigation.closeDrawer();
}}
>
<Icon name="logout" size={20} color="#ff4444" />
<Text style={styles.logoutText}>Logout</Text>
</TouchableOpacity>
</View>
</DrawerContentScrollView>
);
}
function AppDrawerNavigator() {
return (
<Drawer.Navigator
initialRouteName="Dashboard"
drawerContent={(props) => <CustomDrawerContent {...props} />}
screenOptions={{
drawerStyle: {
backgroundColor: "#fff",
width: 280,
},
drawerActiveTintColor: "#6200ee",
drawerInactiveTintColor: "#666",
drawerLabelStyle: {
fontSize: 16,
fontWeight: "500",
},
headerStyle: {
backgroundColor: "#6200ee",
},
headerTintColor: "#fff",
headerTitleStyle: {
fontWeight: "bold",
},
}}
>
<Drawer.Screen
name="Dashboard"
component={DashboardScreen}
options={{
drawerIcon: ({ color, size }) => (
<Icon name="dashboard" color={color} size={size} />
),
}}
/>
<Drawer.Screen
name="Projects"
component={ProjectsScreen}
options={{
drawerIcon: ({ color, size }) => (
<Icon name="folder" color={color} size={size} />
),
}}
/>
<Drawer.Screen
name="Settings"
component={SettingsScreen}
options={{
drawerIcon: ({ color, size }) => (
<Icon name="settings" color={color} size={size} />
),
}}
/>
</Drawer.Navigator>
);
}
const styles = StyleSheet.create({
drawerContainer: {
flex: 1,
},
profileSection: {
padding: 20,
borderBottomWidth: 1,
borderBottomColor: "#e0e0e0",
alignItems: "center",
},
profileImage: {
width: 80,
height: 80,
borderRadius: 40,
marginBottom: 10,
},
userName: {
fontSize: 18,
fontWeight: "bold",
color: "#333",
},
userEmail: {
fontSize: 14,
color: "#666",
marginTop: 5,
},
drawerItems: {
flex: 1,
paddingTop: 10,
},
logoutSection: {
borderTopWidth: 1,
borderTopColor: "#e0e0e0",
padding: 20,
},
logoutButton: {
flexDirection: "row",
alignItems: "center",
},
logoutText: {
marginLeft: 10,
fontSize: 16,
color: "#ff4444",
fontWeight: "500",
},
});
Advanced Navigation Patterns
Nested Navigators
// Complex nested navigation structure
import React from "react";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { createDrawerNavigator } from "@react-navigation/drawer";
const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();
const Drawer = createDrawerNavigator();
// Home Stack Navigator
function HomeStackNavigator() {
return (
<Stack.Navigator
screenOptions={{
headerShown: false, // Hide headers for tab screens
}}
>
<Stack.Screen name="HomeScreen" component={HomeScreen} />
<Stack.Screen
name="PostDetail"
component={PostDetailScreen}
options={{
headerShown: true,
title: "Post Details",
presentation: "modal",
}}
/>
<Stack.Screen
name="Comments"
component={CommentsScreen}
options={{
headerShown: true,
title: "Comments",
}}
/>
</Stack.Navigator>
);
}
// Profile Stack Navigator
function ProfileStackNavigator() {
return (
<Stack.Navigator>
<Stack.Screen name="ProfileScreen" component={ProfileScreen} />
<Stack.Screen name="EditProfile" component={EditProfileScreen} />
<Stack.Screen name="ChangePassword" component={ChangePasswordScreen} />
</Stack.Navigator>
);
}
// Main Tab Navigator
function MainTabNavigator() {
return (
<Tab.Navigator>
<Tab.Screen
name="Home"
component={HomeStackNavigator}
options={{ headerShown: false }}
/>
<Tab.Screen name="Search" component={SearchScreen} />
<Tab.Screen name="Notifications" component={NotificationsScreen} />
<Tab.Screen
name="Profile"
component={ProfileStackNavigator}
options={{ headerShown: false }}
/>
</Tab.Navigator>
);
}
// Root Navigator with Drawer
function RootNavigator() {
return (
<Drawer.Navigator>
<Drawer.Screen
name="MainTabs"
component={MainTabNavigator}
options={{ title: "Dashboard" }}
/>
<Drawer.Screen name="Settings" component={SettingsScreen} />
<Drawer.Screen name="Help" component={HelpScreen} />
</Drawer.Navigator>
);
}
// App with conditional navigation based on auth state
export default function App() {
const [isAuthenticated, setIsAuthenticated] = React.useState(false);
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
// Check authentication status
checkAuthStatus().then((authenticated) => {
setIsAuthenticated(authenticated);
setIsLoading(false);
});
}, []);
if (isLoading) {
return <LoadingScreen />;
}
return (
<NavigationContainer>
{isAuthenticated ? <RootNavigator /> : <AuthStackNavigator />}
</NavigationContainer>
);
}
Modal Navigation
// Modal navigation patterns
import React from "react";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
const Stack = createNativeStackNavigator();
function AppNavigator() {
return (
<Stack.Navigator>
{/* Main app screens */}
<Stack.Group>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Group>
{/* Modal screens */}
<Stack.Group screenOptions={{ presentation: "modal" }}>
<Stack.Screen
name="CreatePost"
component={CreatePostScreen}
options={{
title: "Create Post",
headerLeft: () => (
<TouchableOpacity onPress={() => navigation.goBack()}>
<Text style={{ color: "#6200ee" }}>Cancel</Text>
</TouchableOpacity>
),
headerRight: () => (
<TouchableOpacity onPress={handleSave}>
<Text style={{ color: "#6200ee", fontWeight: "bold" }}>
Save
</Text>
</TouchableOpacity>
),
}}
/>
<Stack.Screen
name="ImagePicker"
component={ImagePickerScreen}
options={{
title: "Select Image",
presentation: "fullScreenModal",
animation: "slide_from_bottom",
}}
/>
</Stack.Group>
{/* Overlay screens */}
<Stack.Group
screenOptions={{
presentation: "transparentModal",
headerShown: false,
animation: "fade",
}}
>
<Stack.Screen name="Alert" component={CustomAlertScreen} />
<Stack.Screen name="Loading" component={LoadingOverlayScreen} />
</Stack.Group>
</Stack.Navigator>
);
}
// Modal screen implementation
function CreatePostScreen({ navigation }) {
const [title, setTitle] = React.useState("");
const [content, setContent] = React.useState("");
const handleSave = async () => {
try {
await savePost({ title, content });
navigation.goBack();
} catch (error) {
console.error("Failed to save post:", error);
}
};
React.useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity
onPress={handleSave}
disabled={!title.trim() || !content.trim()}
>
<Text
style={{
color: title.trim() && content.trim() ? "#6200ee" : "#ccc",
fontWeight: "bold",
}}
>
Save
</Text>
</TouchableOpacity>
),
});
}, [navigation, title, content]);
return (
<View style={styles.container}>
<TextInput
style={styles.titleInput}
placeholder="Post title"
value={title}
onChangeText={setTitle}
/>
<TextInput
style={styles.contentInput}
placeholder="What's on your mind?"
multiline
value={content}
onChangeText={setContent}
/>
</View>
);
}
Deep Linking
// Deep linking configuration
import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { Linking } from "react-native";
const linking = {
prefixes: ["myapp://", "https://myapp.com"],
config: {
screens: {
// Main navigation
Home: "home",
Profile: {
path: "/profile/:userId",
parse: {
userId: (userId) => userId,
},
},
PostDetail: {
path: "/post/:postId",
parse: {
postId: (postId) => postId,
},
},
// Nested navigation
MainTabs: {
screens: {
Home: {
screens: {
HomeScreen: "feed",
PostDetail: "feed/post/:postId",
},
},
Search: {
path: "/search",
screens: {
SearchScreen: "",
SearchResults: "results/:query",
},
},
},
},
// Modal screens
Settings: "settings",
EditProfile: "profile/edit",
},
},
};
export default function App() {
return (
<NavigationContainer
linking={linking}
fallback={<LoadingScreen />}
onReady={() => {
console.log("Navigation container is ready");
}}
onStateChange={(state) => {
console.log("Navigation state changed:", state);
}}
>
<RootNavigator />
</NavigationContainer>
);
}
// Handle deep links in components
function PostDetailScreen({ route, navigation }) {
const { postId } = route.params;
const [post, setPost] = React.useState(null);
React.useEffect(() => {
// Fetch post data based on postId from deep link
fetchPost(postId)
.then(setPost)
.catch((error) => {
console.error("Failed to fetch post:", error);
navigation.goBack();
});
}, [postId]);
if (!post) {
return <LoadingScreen />;
}
return (
<View>
<Text>{post.title}</Text>
<Text>{post.content}</Text>
</View>
);
}
// Custom hook for handling deep links
function useDeepLinking() {
React.useEffect(() => {
const handleDeepLink = (url) => {
console.log("Deep link received:", url);
// Handle custom deep link logic
};
// Handle app launch from deep link
Linking.getInitialURL().then((url) => {
if (url) {
handleDeepLink(url);
}
});
// Handle deep links when app is running
const subscription = Linking.addEventListener("url", ({ url }) => {
handleDeepLink(url);
});
return () => subscription?.remove();
}, []);
}
Navigation State Management
Navigation State Persistence
// Navigation state persistence
import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import AsyncStorage from "@react-native-async-storage/async-storage";
const PERSISTENCE_KEY = "NAVIGATION_STATE_V1";
export default function App() {
const [isReady, setIsReady] = React.useState(false);
const [initialState, setInitialState] = React.useState();
React.useEffect(() => {
const restoreState = async () => {
try {
const initialUrl = await Linking.getInitialURL();
if (Platform.OS !== "web" && initialUrl == null) {
// Only restore state if there's no deep link and we're not on web
const savedStateString = await AsyncStorage.getItem(PERSISTENCE_KEY);
const state = savedStateString
? JSON.parse(savedStateString)
: undefined;
if (state !== undefined) {
setInitialState(state);
}
}
} finally {
setIsReady(true);
}
};
if (!isReady) {
restoreState();
}
}, [isReady]);
const handleStateChange = (state) => {
AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state));
};
if (!isReady) {
return null;
}
return (
<NavigationContainer
initialState={initialState}
onStateChange={handleStateChange}
>
<RootNavigator />
</NavigationContainer>
);
}
Navigation Events and Listeners
// Navigation events and listeners
import React from "react";
import { useFocusEffect, useNavigation } from "@react-navigation/native";
function HomeScreen() {
const navigation = useNavigation();
// Focus effect - runs when screen comes into focus
useFocusEffect(
React.useCallback(() => {
console.log("Screen focused");
// Fetch fresh data
fetchHomeData();
// Cleanup function
return () => {
console.log("Screen unfocused");
};
}, [])
);
// Navigation listeners
React.useEffect(() => {
const unsubscribeBeforeRemove = navigation.addListener(
"beforeRemove",
(e) => {
// Prevent default behavior of leaving the screen
e.preventDefault();
// Show confirmation dialog
Alert.alert(
"Discard changes?",
"You have unsaved changes. Are you sure to discard them?",
[
{ text: "Don't leave", style: "cancel", onPress: () => {} },
{
text: "Discard",
style: "destructive",
onPress: () => navigation.dispatch(e.data.action),
},
]
);
}
);
const unsubscribeTabPress = navigation.addListener("tabPress", (e) => {
// Prevent default behavior
e.preventDefault();
// Custom tab press handling
console.log("Tab pressed");
});
return () => {
unsubscribeBeforeRemove();
unsubscribeTabPress();
};
}, [navigation]);
return <View>{/* Screen content */}</View>;
}
// Global navigation event handling
function useNavigationEvents() {
const navigation = useNavigation();
React.useEffect(() => {
const unsubscribe = navigation.addListener("state", (e) => {
// Track navigation for analytics
const currentRoute = navigation.getCurrentRoute();
analytics.track("screen_view", {
screen_name: currentRoute?.name,
screen_params: currentRoute?.params,
});
});
return unsubscribe;
}, [navigation]);
}
Performance Optimization
Lazy Loading and Code Splitting
// Lazy loading screens for better performance
import React from "react";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
const Stack = createNativeStackNavigator();
// Lazy load screens
const HomeScreen = React.lazy(() => import("./screens/HomeScreen"));
const ProfileScreen = React.lazy(() => import("./screens/ProfileScreen"));
const SettingsScreen = React.lazy(() => import("./screens/SettingsScreen"));
// Loading component
function LoadingScreen() {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#6200ee" />
<Text>Loading...</Text>
</View>
);
}
function AppNavigator() {
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={React.memo(() => (
<React.Suspense fallback={<LoadingScreen />}>
<HomeScreen />
</React.Suspense>
))}
/>
<Stack.Screen
name="Profile"
component={React.memo(() => (
<React.Suspense fallback={<LoadingScreen />}>
<ProfileScreen />
</React.Suspense>
))}
/>
<Stack.Screen
name="Settings"
component={React.memo(() => (
<React.Suspense fallback={<LoadingScreen />}>
<SettingsScreen />
</React.Suspense>
))}
/>
</Stack.Navigator>
);
}
// Optimized screen component with memo
const OptimizedHomeScreen = React.memo(function HomeScreen({
navigation,
route,
}) {
const [data, setData] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const fetchData = React.useCallback(async () => {
try {
setLoading(true);
const result = await api.fetchHomeData();
setData(result);
} catch (error) {
console.error("Failed to fetch data:", error);
} finally {
setLoading(false);
}
}, []);
useFocusEffect(
React.useCallback(() => {
fetchData();
}, [fetchData])
);
if (loading) {
return <LoadingScreen />;
}
return (
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <ListItem item={item} />}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={10}
initialNumToRender={5}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>
);
});
Navigation Performance Optimizations
// Navigation performance optimizations
import React from "react";
import { InteractionManager } from "react-native";
// Optimize screen transitions
function OptimizedScreen({ navigation }) {
const [isReady, setIsReady] = React.useState(false);
React.useEffect(() => {
const task = InteractionManager.runAfterInteractions(() => {
setIsReady(true);
});
return () => task.cancel();
}, []);
if (!isReady) {
return <LoadingScreen />;
}
return <View>{/* Screen content */}</View>;
}
// Preload screens for faster navigation
function usePreloadScreens() {
React.useEffect(() => {
const preloadScreens = async () => {
// Preload critical screens
await Promise.all([
import("./screens/ProfileScreen"),
import("./screens/SearchScreen"),
]);
};
const timer = setTimeout(preloadScreens, 1000);
return () => clearTimeout(timer);
}, []);
}
// Optimize re-renders with navigation context
const NavigationContext = React.createContext();
function NavigationProvider({ children }) {
const [currentRoute, setCurrentRoute] = React.useState("Home");
const navigationState = React.useMemo(
() => ({
currentRoute,
setCurrentRoute,
}),
[currentRoute]
);
return (
<NavigationContext.Provider value={navigationState}>
{children}
</NavigationContext.Provider>
);
}
// Custom hook for navigation state
function useNavigationState() {
const context = React.useContext(NavigationContext);
if (!context) {
throw new Error(
"useNavigationState must be used within NavigationProvider"
);
}
return context;
}
Custom Navigation Components
Custom Tab Bar
// Custom tab bar implementation
import React from "react";
import {
View,
TouchableOpacity,
Text,
Animated,
Dimensions,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Icon from "react-native-vector-icons/MaterialIcons";
const { width } = Dimensions.get("window");
function CustomTabBar({ state, descriptors, navigation }) {
const insets = useSafeAreaInsets();
const tabWidth = width / state.routes.length;
// Animated indicator
const translateX = React.useRef(new Animated.Value(0)).current;
React.useEffect(() => {
Animated.spring(translateX, {
toValue: state.index * tabWidth,
useNativeDriver: true,
tension: 100,
friction: 8,
}).start();
}, [state.index, tabWidth]);
return (
<View style={[styles.tabBarContainer, { paddingBottom: insets.bottom }]}>
{/* Animated indicator */}
<Animated.View
style={[
styles.tabIndicator,
{
width: tabWidth,
transform: [{ translateX }],
},
]}
/>
{/* Tab buttons */}
{state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const label = options.tabBarLabel || options.title || route.name;
const isFocused = state.index === index;
const onPress = () => {
const event = navigation.emit({
type: "tabPress",
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name);
}
};
const onLongPress = () => {
navigation.emit({
type: "tabLongPress",
target: route.key,
});
};
// Icon mapping
const getIconName = (routeName) => {
switch (routeName) {
case "Home":
return "home";
case "Search":
return "search";
case "Notifications":
return "notifications";
case "Profile":
return "person";
default:
return "help";
}
};
return (
<TouchableOpacity
key={route.key}
accessibilityRole="button"
accessibilityState={isFocused ? { selected: true } : {}}
accessibilityLabel={options.tabBarAccessibilityLabel}
testID={options.tabBarTestID}
onPress={onPress}
onLongPress={onLongPress}
style={styles.tabButton}
>
<View style={styles.tabContent}>
<Icon
name={getIconName(route.name)}
size={24}
color={isFocused ? "#6200ee" : "#666"}
/>
<Text
style={[
styles.tabLabel,
{ color: isFocused ? "#6200ee" : "#666" },
]}
>
{label}
</Text>
</View>
</TouchableOpacity>
);
})}
</View>
);
}
const styles = StyleSheet.create({
tabBarContainer: {
flexDirection: "row",
backgroundColor: "#fff",
borderTopWidth: 1,
borderTopColor: "#e0e0e0",
paddingTop: 8,
elevation: 8,
shadowOpacity: 0.1,
shadowRadius: 4,
shadowColor: "#000",
shadowOffset: {
height: -2,
width: 0,
},
},
tabIndicator: {
position: "absolute",
top: 0,
height: 3,
backgroundColor: "#6200ee",
},
tabButton: {
flex: 1,
alignItems: "center",
paddingVertical: 8,
},
tabContent: {
alignItems: "center",
},
tabLabel: {
fontSize: 12,
fontWeight: "500",
marginTop: 4,
},
});
Custom Header
// Custom header implementation
import React from "react";
import { View, Text, TouchableOpacity, Animated } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Icon from "react-native-vector-icons/MaterialIcons";
function CustomHeader({
title,
subtitle,
leftButton,
rightButton,
backgroundColor = "#6200ee",
textColor = "#fff",
onTitlePress,
showSubtitle = false,
}) {
const insets = useSafeAreaInsets();
const scrollY = React.useRef(new Animated.Value(0)).current;
// Animated header opacity based on scroll
const headerOpacity = scrollY.interpolate({
inputRange: [0, 100],
outputRange: [0.9, 1],
extrapolate: "clamp",
});
const titleScale = scrollY.interpolate({
inputRange: [0, 50],
outputRange: [1.2, 1],
extrapolate: "clamp",
});
return (
<Animated.View
style={[
styles.headerContainer,
{
backgroundColor,
paddingTop: insets.top,
opacity: headerOpacity,
},
]}
>
<View style={styles.headerContent}>
{/* Left button */}
<View style={styles.headerSide}>
{leftButton && (
<TouchableOpacity
style={styles.headerButton}
onPress={leftButton.onPress}
>
<Icon name={leftButton.icon} size={24} color={textColor} />
</TouchableOpacity>
)}
</View>
{/* Title section */}
<TouchableOpacity
style={styles.titleContainer}
onPress={onTitlePress}
disabled={!onTitlePress}
>
<Animated.Text
style={[
styles.headerTitle,
{
color: textColor,
transform: [{ scale: titleScale }],
},
]}
numberOfLines={1}
>
{title}
</Animated.Text>
{showSubtitle && subtitle && (
<Text style={[styles.headerSubtitle, { color: textColor }]}>
{subtitle}
</Text>
)}
</TouchableOpacity>
{/* Right button */}
<View style={styles.headerSide}>
{rightButton && (
<TouchableOpacity
style={styles.headerButton}
onPress={rightButton.onPress}
>
{rightButton.icon ? (
<Icon name={rightButton.icon} size={24} color={textColor} />
) : (
<Text style={[styles.headerButtonText, { color: textColor }]}>
{rightButton.text}
</Text>
)}
</TouchableOpacity>
)}
</View>
</View>
</Animated.View>
);
}
// Usage in screen
function ScreenWithCustomHeader({ navigation }) {
return (
<View style={styles.container}>
<CustomHeader
title="Custom Header"
subtitle="Subtitle here"
showSubtitle={true}
leftButton={{
icon: "arrow-back",
onPress: () => navigation.goBack(),
}}
rightButton={{
icon: "more-vert",
onPress: () => console.log("More pressed"),
}}
onTitlePress={() => console.log("Title pressed")}
/>
<View style={styles.content}>{/* Screen content */}</View>
</View>
);
}
const styles = StyleSheet.create({
headerContainer: {
elevation: 4,
shadowOpacity: 0.2,
shadowRadius: 4,
shadowColor: "#000",
shadowOffset: {
height: 2,
width: 0,
},
},
headerContent: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
paddingVertical: 12,
minHeight: 56,
},
headerSide: {
width: 40,
alignItems: "center",
},
headerButton: {
padding: 8,
},
headerButtonText: {
fontSize: 16,
fontWeight: "500",
},
titleContainer: {
flex: 1,
alignItems: "center",
},
headerTitle: {
fontSize: 20,
fontWeight: "bold",
},
headerSubtitle: {
fontSize: 14,
opacity: 0.8,
marginTop: 2,
},
container: {
flex: 1,
},
content: {
flex: 1,
},
});
Testing Navigation
Unit Testing Navigation
// Testing navigation with Jest and React Native Testing Library
import React from "react";
import { render, fireEvent, waitFor } from "@testing-library/react-native";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import HomeScreen from "../screens/HomeScreen";
import ProfileScreen from "../screens/ProfileScreen";
const Stack = createNativeStackNavigator();
function TestNavigator({ initialRouteName = "Home" }) {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName={initialRouteName}>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
describe("Navigation Tests", () => {
it("should navigate to profile screen when button is pressed", async () => {
const { getByText } = render(<TestNavigator />);
const navigateButton = getByText("Go to Profile");
fireEvent.press(navigateButton);
await waitFor(() => {
expect(getByText("Profile Screen")).toBeTruthy();
});
});
it("should pass correct parameters when navigating", async () => {
const { getByText } = render(<TestNavigator />);
const navigateButton = getByText("View User Profile");
fireEvent.press(navigateButton);
await waitFor(() => {
expect(getByText("User: John Doe")).toBeTruthy();
});
});
it("should go back to previous screen", async () => {
const { getByText } = render(<TestNavigator initialRouteName="Profile" />);
const backButton = getByText("Back");
fireEvent.press(backButton);
await waitFor(() => {
expect(getByText("Home Screen")).toBeTruthy();
});
});
});
// Mock navigation for component testing
const createMockNavigation = (overrides = {}) => ({
navigate: jest.fn(),
goBack: jest.fn(),
replace: jest.fn(),
reset: jest.fn(),
setOptions: jest.fn(),
addListener: jest.fn(() => jest.fn()),
removeListener: jest.fn(),
...overrides,
});
// Test component with mocked navigation
describe("HomeScreen Component", () => {
it("should call navigate with correct parameters", () => {
const mockNavigation = createMockNavigation();
const mockRoute = { params: {} };
const { getByText } = render(
<HomeScreen navigation={mockNavigation} route={mockRoute} />
);
const profileButton = getByText("Go to Profile");
fireEvent.press(profileButton);
expect(mockNavigation.navigate).toHaveBeenCalledWith("Profile", {
userId: "12345",
userName: "John Doe",
});
});
});
Integration Testing
// E2E testing with Detox
describe("Navigation Flow", () => {
beforeAll(async () => {
await device.launchApp();
});
beforeEach(async () => {
await device.reloadReactNative();
});
it("should navigate through main flow", async () => {
// Test tab navigation
await element(by.text("Home")).tap();
await expect(element(by.text("Welcome Home"))).toBeVisible();
await element(by.text("Search")).tap();
await expect(element(by.text("Search Screen"))).toBeVisible();
// Test stack navigation
await element(by.text("View Profile")).tap();
await expect(element(by.text("User Profile"))).toBeVisible();
// Test going back
await element(by.text("Back")).tap();
await expect(element(by.text("Search Screen"))).toBeVisible();
});
it("should handle deep links correctly", async () => {
await device.openURL({
url: "myapp://profile/12345",
});
await expect(element(by.text("Profile Details"))).toBeVisible();
await expect(element(by.text("User ID: 12345"))).toBeVisible();
});
it("should persist navigation state", async () => {
// Navigate to a specific screen
await element(by.text("Settings")).tap();
await expect(element(by.text("Settings Screen"))).toBeVisible();
// Background and relaunch app
await device.sendToHome();
await device.launchApp({ newInstance: false });
// Should be on the same screen
await expect(element(by.text("Settings Screen"))).toBeVisible();
});
});
Best Practices and Common Patterns
Navigation Best Practices
// Navigation best practices implementation
const navigationBestPractices = {
// 1. Use proper TypeScript types
typeSafety: `
// Define param lists for type safety
type RootStackParamList = {
Home: undefined;
Profile: { userId: string };
Settings: undefined;
};
// Use typed navigation hooks
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const route = useRoute<RouteProp<RootStackParamList, 'Profile'>>();
`,
// 2. Optimize performance
performance: {
lazyLoading: "Use React.lazy() for non-critical screens",
memoization: "Memo screens to prevent unnecessary re-renders",
preloading: "Preload critical screens after app launch",
codeExample: `
const LazyScreen = React.lazy(() => import('./LazyScreen'));
const MemoizedScreen = React.memo(MyScreen);
// Use InteractionManager for heavy operations
React.useEffect(() => {
InteractionManager.runAfterInteractions(() => {
// Heavy operations here
});
}, []);
`,
},
// 3. Handle navigation state properly
stateManagement: {
persistence: "Implement navigation state persistence",
resetOnLogout: "Reset navigation stack on logout",
deepLinking: "Handle deep links gracefully",
codeExample: `
// Reset navigation stack on logout
const logout = () => {
navigation.reset({
index: 0,
routes: [{ name: 'Login' }],
});
};
// Handle deep links with error boundaries
const handleDeepLink = (url) => {
try {
const route = parseDeepLink(url);
navigation.navigate(route.name, route.params);
} catch (error) {
console.error('Invalid deep link:', error);
navigation.navigate('Home');
}
};
`,
},
// 4. Accessibility
accessibility: {
labels: "Provide proper accessibility labels",
announcements: "Use screen reader announcements",
navigation: "Support keyboard navigation",
codeExample: `
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel="Navigate to profile"
accessibilityHint="Opens your user profile screen"
onPress={() => navigation.navigate('Profile')}
>
<Text>Profile</Text>
</TouchableOpacity>
`,
},
};
// Error boundaries for navigation
class NavigationErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error("Navigation error:", error, errorInfo);
// Log to crash reporting service
}
render() {
if (this.state.hasError) {
return (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>Something went wrong</Text>
<Button
title="Go to Home"
onPress={() => {
this.setState({ hasError: false });
this.props.navigation.navigate("Home");
}}
/>
</View>
);
}
return this.props.children;
}
}
Common Navigation Patterns
// Common navigation patterns and solutions
const commonPatterns = {
// 1. Conditional navigation based on auth state
conditionalNavigation: `
function RootNavigator() {
const { user, loading } = useAuth();
if (loading) {
return <LoadingScreen />;
}
return user ? <AuthenticatedNavigator /> : <AuthNavigator />;
}
`,
// 2. Tab navigator with nested stacks
nestedTabStacks: `
function HomeStackNavigator() {
return (
<Stack.Navigator>
<Stack.Screen name="HomeScreen" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
);
}
function MainTabNavigator() {
return (
<Tab.Navigator>
<Tab.Screen
name="Home"
component={HomeStackNavigator}
options={{ headerShown: false }}
/>
</Tab.Navigator>
);
}
`,
// 3. Modal navigation pattern
modalPattern: `
function ModalNavigator() {
return (
<Stack.Navigator>
<Stack.Group>
<Stack.Screen name="Main" component={MainNavigator} />
</Stack.Group>
<Stack.Group screenOptions={{ presentation: 'modal' }}>
<Stack.Screen name="Settings" component={SettingsScreen} />
<Stack.Screen name="CreatePost" component={CreatePostScreen} />
</Stack.Group>
</Stack.Navigator>
);
}
`,
// 4. Bottom sheet navigation
bottomSheetPattern: `
function BottomSheetNavigator() {
const [isVisible, setIsVisible] = React.useState(false);
return (
<View style={styles.container}>
<MainContent />
<BottomSheet
isVisible={isVisible}
onClose={() => setIsVisible(false)}
>
<NavigationContainer independent={true}>
<Stack.Navigator>
<Stack.Screen name="Option1" component={Option1Screen} />
<Stack.Screen name="Option2" component={Option2Screen} />
</Stack.Navigator>
</NavigationContainer>
</BottomSheet>
</View>
);
}
`,
};
Conclusion
React Navigation v6+ has transformed mobile app development in React Native by providing a powerful, flexible, and performant navigation solution. This guide covered the essential concepts and advanced patterns needed to build professional mobile applications.
Key Takeaways
- Modern API: React Navigation v6 offers a component-based API that's more intuitive and type-safe
- Performance: Proper implementation with lazy loading and optimization techniques ensures smooth navigation
- Flexibility: Support for complex navigation patterns including nested navigators, modals, and deep linking
- TypeScript: Full TypeScript support improves development experience and reduces runtime errors
- Customization: Extensive customization options for headers, tabs, and navigation behavior
Best Practices Summary
- Always use TypeScript for better type safety and development experience
- Implement proper navigation state persistence for better user experience
- Optimize performance with lazy loading and memoization
- Handle deep links gracefully with error boundaries
- Test navigation flows thoroughly with both unit and integration tests
- Follow accessibility guidelines for inclusive app design
Next Steps
- Implement navigation state management with Redux or Context
- Add navigation analytics and user behavior tracking
- Explore advanced animation patterns with Reanimated
- Build custom navigation components for unique app experiences
- Implement offline navigation state handling
React Navigation v6+ provides all the tools needed to create exceptional navigation experiences in React Native applications. Master these patterns and techniques to build apps that users love to navigate through.