Navigation with Expo Router
PHL RN Boilerplate uses Expo Router for file-based navigation, providing a powerful and intuitive routing system similar to Next.js.
File-Based Routing
Every file in the src/app/ directory automatically becomes a route. No need to manually configure routes!
src/app/
├── _layout.tsx # Root layout
├── index.tsx # / (home)
├── about.tsx # /about
├── profile.tsx # /profile
└── user/
└── [id].tsx # /user/:id (dynamic)Basic Navigation
Using Link Component
import { Link } from 'expo-router';
function HomeScreen() {
return (
<Link href="/profile">
Go to Profile
</Link>
);
}Using Router Hooks
import { router } from 'expo-router';
function MyComponent() {
const handleNavigate = () => {
router.push('/profile');
};
return (
<Button onPress={handleNavigate}>
Go to Profile
</Button>
);
}Navigation Methods
| Method | Description |
|---|---|
| router.push() | Navigate to a new screen (adds to history) |
| router.replace() | Replace current screen (no history) |
| router.back() | Go back to previous screen |
| router.canGoBack() | Check if can go back |
Dynamic Routes
Create dynamic routes using square brackets in the filename:
import { useLocalSearchParams } from 'expo-router';
export default function UserScreen() {
const { id } = useLocalSearchParams();
return (
<View>
<Text>User ID: {id}</Text>
</View>
);
}
// Navigate: router.push('/user/123')
// Params: id = "123"Route Groups
Organize routes without affecting the URL structure using parentheses:
src/app/
├── (tabs)/ # Group: doesn't appear in URL
│ ├── _layout.tsx # Tabs layout
│ ├── index.tsx # /
│ └── settings.tsx # /settings
└── (auth)/ # Another group
├── login.tsx # /login
└── register.tsx # /registerLayouts
Layouts wrap child routes and persist across navigation:
import { Tabs } from 'expo-router';
export default function TabLayout() {
return (
<Tabs>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color }) => <HomeIcon color={color} />,
}}
/>
<Tabs.Screen
name="settings"
options={{
title: 'Settings',
tabBarIcon: ({ color }) => <SettingsIcon color={color} />,
}}
/>
</Tabs>
);
}Passing Parameters
URL Parameters
// Navigate with params
router.push({
pathname: '/user/[id]',
params: { id: '123', name: 'John' }
});
// In the target screen
const { id, name } = useLocalSearchParams();
// id = "123", name = "John"Query Parameters
// Navigate with query params
router.push('/search?q=react&sort=recent');
// In the target screen
const { q, sort } = useLocalSearchParams();
// q = "react", sort = "recent"Navigation Options
Customize screen options in the route file:
import { Stack } from 'expo-router';
export default function ProfileScreen() {
return (
<>
<Stack.Screen
options={{
title: 'My Profile',
headerShown: true,
headerStyle: { backgroundColor: '#0f172a' },
headerTintColor: '#fff',
}}
/>
<View>
{/* Screen content */}
</View>
</>
);
}Deep Linking
Expo Router automatically supports deep linking. Configure your app scheme in app.json:
{
"expo": {
"scheme": "myapp",
"ios": {
"bundleIdentifier": "com.mycompany.myapp"
},
"android": {
"package": "com.mycompany.myapp"
}
}
}Now users can open your app with URLs:
myapp://→ Home screenmyapp://profile→ Profile screenmyapp://user/123→ User screen with id=123
Navigation in ViewModels
Following the MVVM pattern, handle navigation in ViewModels:
import { useCallback } from 'react';
import { router } from 'expo-router';
export function useProfileViewModel() {
const handleEditProfile = useCallback(() => {
router.push('/edit-profile');
}, []);
const handleLogout = useCallback(() => {
// Clear auth state
router.replace('/login');
}, []);
return {
handleEditProfile,
handleLogout,
};
}Common Patterns
Protected Routes
import { useAuth } from '@/hooks/useAuth';
import { Redirect } from 'expo-router';
export default function ProtectedScreen() {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Redirect href="/login" />;
}
return <View>{/* Protected content */}</View>;
}Modal Routes
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{
presentation: 'modal',
headerTitle: 'Modal Screen',
}}
/>
</Stack>
);
}Nested Navigation
src/app/
├── _layout.tsx # Root Stack
├── (tabs)/
│ ├── _layout.tsx # Tabs Navigator
│ ├── index.tsx # Tab 1
│ └── shop/
│ ├── _layout.tsx # Shop Stack
│ ├── index.tsx # Shop home
│ └── [id].tsx # Product detailBest Practices
✅ Use type-safe navigation
Leverage TypeScript to catch navigation errors at compile time.
✅ Keep navigation in ViewModels
Don't navigate directly from Views - let ViewModels handle it.
✅ Use route groups
Organize related screens without cluttering URLs.
✅ Configure deep linking
Set up your app scheme for better user experience.