← Back to Lessons

Advanced Navigation in React Native: Nested Routes, Headers, and Conditional Flows

Nested Routes: Organize Complexity

Once you master the basics of navigation in React Native, the next natural step is to tackle more complex scenarios: authentication flows, deeply nested routes, headers that change based on context, and screens that appear or disappear depending on your app's state.

These patterns are what separate a functional app from a professional app. And while they may seem intimidating at first, once you understand the logic behind each one, they become powerful tools for building sophisticated experiences.

Nested Routes: Organize Complexity

In real applications, navigation is rarely flat. Imagine an e-commerce app: you have main tabs (home, cart, profile), but inside "home" you need a stack to navigate between categories, products, and details. Inside "profile", another stack to edit data, view history, manage addresses.

This is nested navigation; navigation within navigation.

📁 Typical Structure

1app/ 2 ├─ (tabs)/ 3 │ ├─ _layout.tsx → Tab Navigator 4 │ ├─ index.tsx → "Home" Tab 5 │ ├─ cart.tsx → "Cart" Tab 6 │ └─ profile/ 7 │ ├─ _layout.tsx → Internal Stack Navigator 8 │ ├─ index.tsx → Profile main screen 9 │ ├─ edit.tsx → Edit profile 10 │ └─ history.tsx → Order history 11 └─ _layout.tsx → Root layout

In this case, the "Profile" tab is not just a screen: it's a full stack. When the user enters their profile, they can navigate to edit data or view history, and when they go back, they return to the main profile screen, not the previous tab.

Practical Implementation

Main Tab Navigator:

1// app/(tabs)/_layout.tsx 2import { Tabs } from "expo-router"; 3import { Home, ShoppingCart, User } from "lucide-react-native"; 4 5export default function TabsLayout() { 6 return ( 7 <Tabs> 8 <Tabs.Screen 9 name="index" 10 options={{ 11 title: "Home", 12 tabBarIcon: ({ color }) => <Home color={color} size={24} /> 13 }} 14 /> 15 <Tabs.Screen 16 name="cart" 17 options={{ 18 title: "Cart", 19 tabBarIcon: ({ color }) => <ShoppingCart color={color} size={24} /> 20 }} 21 /> 22 <Tabs.Screen 23 name="profile" 24 options={{ 25 title: "Profile", 26 tabBarIcon: ({ color }) => <User color={color} size={24} /> 27 }} 28 /> 29 </Tabs> 30 ); 31}

Internal Profile Stack:

1// app/(tabs)/profile/_layout.tsx 2import { Stack } from "expo-router"; 3 4export default function ProfileLayout() { 5 return ( 6 <Stack> 7 <Stack.Screen 8 name="index" 9 options={{ headerShown: false }} // Tab already shows header 10 /> 11 <Stack.Screen 12 name="edit" 13 options={{ title: "Edit Profile" }} 14 /> 15 <Stack.Screen 16 name="history" 17 options={{ title: "Order History" }} 18 /> 19 </Stack> 20 ); 21}

Now, from the main profile screen, you can navigate like this:

1// app/(tabs)/profile/index.tsx 2import { Link } from "expo-router"; 3 4export default function ProfileScreen() { 5 return ( 6 <View> 7 <Text>Welcome to your profile</Text> 8 <Link href="/profile/edit">Edit Data</Link> 9 <Link href="/profile/history">View History</Link> 10 </View> 11 ); 12}

Custom Headers: More Than a Title

A screen's header is not just aesthetic: it communicates context, offers quick actions, and reinforces the app's identity. By default, Expo Router gives you a basic header, but professional apps need more control.

Basic Customization

You can modify colors, fonts, and header elements from each screen's options:

1<Stack.Screen 2 name="details" 3 options={{ 4 title: "Product Details", 5 headerStyle: { backgroundColor: "#6200ee" }, 6 headerTintColor: "#fff", 7 headerTitleStyle: { fontWeight: "bold" } 8 }} 9/>

This works for simple cases. But what if you need a fully custom header?

Dynamic Header with Custom Component

1<Stack.Screen 2 name="product" 3 options={{ 4 headerTitle: () => <CustomHeader />, 5 headerRight: () => ( 6 <TouchableOpacity onPress={() => console.log("Share")}> 7 <Share2 color="#fff" size={24} /> 8 </TouchableOpacity> 9 ) 10 }} 11/>

You can insert any React component into the header. This opens up endless possibilities: search bars in the header, filter buttons, loading indicators, animations.

Headers That Change on Scroll

A very common pattern in modern apps is the header that hides when scrolling down and reappears when scrolling up. This maximizes content space without sacrificing navigation.

With React Navigation (the foundation of Expo Router), you can achieve this by configuring advanced options:

1<Stack.Screen 2 name="feed" 3 options={{ 4 headerTransparent: true, 5 headerBlurEffect: "light" 6 }} 7/>

Or even better, use Animated to create smooth transitions based on user scroll. While this requires a bit more code, the result is a fluid, professional experience.

Deep Linking: Navigate from Outside the App

Deep linking allows users to open your app on a specific screen from an external link: an email, a push notification, a QR code. It's essential for marketing, retention, and user experience.

With Expo Router, deep linking works automatically thanks to the file-based structure. You just need to configure the URL scheme:

1// app.json 2{ 3 "expo": { 4 "scheme": "myapp" 5 } 6}

For real URLs (https://yourapp.com/profile/42), you need to configure universal links and app links. This requires verification files on your domain, but Expo simplifies the process with EAS:

1{ 2 "expo": { 3 "ios": { 4 "associatedDomains": ["applinks:yourapp.com"] 5 }, 6 "android": { 7 "intentFilters": [ 8 { 9 "action": "VIEW", 10 "data": { "scheme": "https", "host": "yourapp.com" } 11 } 12 ] 13 } 14 } 15}

Custom Transition Animations

By default, React Navigation handles screen transitions: horizontal slide on iOS, fade on Android. But you can customize these animations to create unique experiences.

1<Stack.Screen 2 name="details" 3 options={{ 4 animation: "fade", 5 // or "slide_from_bottom", "slide_from_right", "flip", etc. 6 }} 7/>

For more complex animations, you can use @react-navigation/stack with gesture handlers and reanimated:

1cardStyleInterpolator: ({ current, layouts }) => ({ 2 cardStyle: { 3 transform: [ 4 { 5 translateX: current.progress.interpolate({ 6 inputRange: [0, 1], 7 outputRange: [layouts.screen.width, 0], 8 }), 9 }, 10 ], 11 }, 12})

This requires understanding interpolation and animations, but the result is total control over how screens move. Some premium apps use custom transitions to reinforce their visual identity.

Conclusion

Advanced navigation is not magic; it's well-thought-out architecture. Nested routes, dynamic headers, conditional flows... each technique solves a real problem you'll face in production apps. Mastering these patterns lets you build apps that not only work, but feel professional.