Implementing Multi-Page Routing with React Navigation
In web development, we're accustomed to switching pages by changing the URL in the address bar (e.g., /home or /profile). But in mobile apps, there's no such thing as an address bar.
We must rely on a Navigation Library to manage screen hierarchies. In the React Native ecosystem, the most authoritative and widely adopted solution is React Navigation, and the latest Expo Router has even transformed it into a "File-based Routing" system almost identical to Next.js!
1. What is Expo Router?
If you've worked with older versions of React Native, you likely remember the nightmare of writing endless Stack.Navigator and Stack.Screen components.
In the latest version of Expo, they introduced Expo Router, which borrows directly from Next.js's App Router concept: whatever files you create in your folder automatically become pages!
Your folder structure will look like this:
app/
├── _layout.tsx # Global layout settings
├── index.tsx # Home page (default)
├── profile.tsx # Profile page
└── settings.tsx # Settings page
2. Stack Navigation: Stack-Based Navigation
The most common navigation pattern in mobile apps is the "Stack."
Imagine a stack of playing cards. When you tap a product to view its details, it's like placing a new card on top. When you tap the < Back button in the top-left corner, it's like removing the top card to return to the previous page.
Define a Stack layout in app/_layout.tsx:
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
{/* Customize the header for different pages */}
<Stack.Screen
name="index"
options={{ title: 'Home', headerShown: false }}
/>
<Stack.Screen
name="profile"
options={{ title: 'Profile', presentation: 'modal' }}
/>
<Stack.Screen
name="settings"
options={{ title: 'Settings' }}
/>
</Stack>
);
}
[!TIP] Note the
presentation: 'modal'option. On iOS, this setting makes the new page slide up from the bottom (instead of the default right-to-left), delivering the classic native interaction experience!
3. Navigating Between Pages (Link and router)
With pages set up, how do we navigate between them? Just like Next.js, we have two methods:
Method 1: Using the <Link> Component (Ideal for buttons/text)
import { Link } from 'expo-router';
import { View, Text } from 'react-native';
export default function Home() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Welcome to the Home Page</Text>
{/* asChild binds the click event to the inner TouchableOpacity */}
<Link href="/profile" asChild>
<TouchableOpacity style={{ marginTop: 20, padding: 10, backgroundColor: 'blue' }}>
<Text style={{ color: 'white' }}>Go to Profile</Text>
</TouchableOpacity>
</Link>
</View>
);
}
Method 2: Using the router API (Ideal for function logic)
import { router } from 'expo-router';
function handleLoginSuccess() {
// After successful validation, force navigation programmatically
router.push('/dashboard');
// Or, if you don't want users to go back to the login page, use replace
// router.replace('/dashboard');
}
4. Hands-On: Building an Instagram-Style Bottom Tab Bar
Almost all mainstream apps (like Instagram, Line, and Spotify) use a bottom tab bar. Implementing this in Expo Router is incredibly simple!
First, create a dedicated folder structure for Tabs:
app/
├── _layout.tsx # Root Stack (for pages without tabs, like login)
└── (tabs)/ # Parentheses denote a route group (won't appear in URLs)
├── _layout.tsx # Defines the Bottom Tabs appearance
├── index.tsx # Home Tab
└── explore.tsx # Explore Tab
Now, configure app/(tabs)/_layout.tsx:
import { Tabs } from 'expo-router';
import { Home, Search } from 'lucide-react-native'; // Remember to install the icon package
export default function TabLayout() {
return (
<Tabs screenOptions={{
tabBarActiveTintColor: '#3b82f6', // Active tab color
headerShown: true // Whether to show the header
}}>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color }) => <Home color={color} size={24} />,
}}
/>
<Tabs.Screen
name="explore"
options={{
title: 'Explore',
tabBarIcon: ({ color }) => <Search color={color} size={24} />,
}}
/>
</Tabs>
);
}
That's it! With just a few lines of code, you've created a polished bottom tab bar that adheres to iOS/Android design guidelines, complete with swipe animations and haptic feedback!
5. Passing Parameters (Route Parameters)
If we tap an article in a "Post List" to navigate to a "Post Detail" page, we need to pass the article's ID.
Sending Parameters:
// On the list page
router.push({ pathname: '/post/[id]', params: { id: 123, title: 'Hello' } });
Receiving Parameters:
If your file is named app/post/[id].tsx:
import { useLocalSearchParams } from 'expo-router';
import { View, Text } from 'react-native';
export default function PostDetail() {
// Destructure the passed parameters
const { id, title } = useLocalSearchParams();
return (
<View>
<Text>Currently viewing post ID: {id}</Text>
<Text>Post title: {title}</Text>
</View>
);
}
Mastering the navigation system gives your app a complete skeleton. In the next chapter, we'll dive into features unique to mobile apps: accessing the camera, fetching GPS location, and sending push notifications!
Navigation Params and Typing
Pass data between screens with type-safe params.
Defining Param Types
export type RootStackParamList = {
Home: undefined;
Profile: { userId: number };
Settings: { section?: string };
ProductDetail: { productId: number; title: string };
};
Using Typed Navigation
import { NativeStackScreenProps } from '@react-navigation/native-stack';
type ProfileScreenProps = NativeStackScreenProps<RootStackParamList, 'Profile'>;
export default function ProfileScreen({ route, navigation }: ProfileScreenProps) {
const { userId } = route.params;
return (
<View>
<Text>User ID: {userId}</Text>
<Button
title="Go to Settings"
onPress={() => navigation.navigate('Settings', { section: 'privacy' })}
/>
</View>
);
}
Passing Data Back
// Screen A
navigation.navigate('Settings', {
onSave: (data: string) => {
console.log('Saved:', data);
}
});
// Screen B — call the callback
route.params.onSave('new value');
Deep Linking
Deep links let external URLs open specific screens in your app.
Configuration
const linking = {
prefixes: ['myapp://', 'https://myapp.com'],
config: {
screens: {
Home: '',
Profile: 'user/:userId',
ProductDetail: 'product/:productId',
Settings: 'settings',
},
},
};
// Usage
// myapp://user/123 → opens Profile screen with userId=123
// https://myapp.com/product/456 → opens ProductDetail
Authentication Flow
function RootNavigator() {
const { user } = useAuthStore();
return (
<Stack.Navigator>
{user ? (
<Stack.Screen name="Main" component={MainTabs} />
) : (
<Stack.Screen name="Auth" component={AuthStack} />
)}
</Stack.Navigator>
);
}
Summary
React Navigation provides stack, tab, and drawer navigators for structuring your app. Type-safe params, deep linking, and conditional auth flows are essential for production apps.
Key takeaways:
- Stack navigator: screen-by-screen navigation with back button |
- Tab navigator: bottom tabs for top-level sections |
- Drawer navigator: side menu for complex apps |
- Param types prevent runtime navigation errors |
- Deep linking opens specific screens from URLs |
- Conditional navigation switches between auth and main stacks |
- Navigation.replace() removes current screen from stack |
- navigation.goBack() returns to previous screen |
What's Next: Native Device Features
The next chapter covers camera, GPS, and notifications.