NearYou uses Expo Router for file-based navigation. This guide explains how the navigation is structured and how to add new screens.
Overview
Expo Router provides:
- File-based routing (no manual route configuration)
- Type-safe navigation
- Deep linking support
- Shared layouts and nested navigation
- Modal presentations
Navigation Architecture
The app uses a stack navigator with tab navigation:
Stack (Root)
├── (tabs) - Tab Navigator
│ ├── index - Home Screen
│ └── explore - Explore Screen
└── modal - Modal Screen
Root Layout
The root layout (src/app/_layout.tsx) configures the main navigation structure:
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { useColorScheme } from '@/hooks/use-color-scheme';
export const unstable_settings = {
anchor: '(tabs)',
};
export default function RootLayout() {
const colorScheme = useColorScheme();
return (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
</Stack>
<StatusBar style="auto" />
</ThemeProvider>
);
}
The unstable_settings.anchor configuration sets (tabs) as the initial route. This means the app opens to the tab navigator by default.
Tab Navigation
Tabs are configured in src/app/(tabs)/_layout.tsx:
src/app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import React from 'react';
import { HapticTab } from '@/components/haptic-tab';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
export default function TabLayout() {
const colorScheme = useColorScheme();
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
headerShown: false,
tabBarButton: HapticTab,
}}>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />,
}}
/>
<Tabs.Screen
name="explore"
options={{
title: 'Explore',
tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />,
}}
/>
</Tabs>
);
}
Tab Features
The active tab color automatically adapts to the current theme:tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint
- Light mode:
#0a7ea4 (blue)
- Dark mode:
#fff (white)
Tab buttons provide haptic feedback on iOS when pressed:See src/components/haptic-tab.tsx:1
Adding Screens
Adding a Tab Screen
Create the screen file
Create a new file in src/app/(tabs)/:src/app/(tabs)/profile.tsx
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { StyleSheet } from 'react-native';
export default function ProfileScreen() {
return (
<ThemedView style={styles.container}>
<ThemedText type="title">Profile</ThemedText>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
});
Add to tab layout
Register the tab in src/app/(tabs)/_layout.tsx:src/app/(tabs)/_layout.tsx
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="person.fill" color={color} />
),
}}
/>
Map the icon (if needed)
If using a new SF Symbol, add it to src/components/ui/icon-symbol.tsx:src/components/ui/icon-symbol.tsx
const MAPPING = {
'house.fill': 'home',
'paperplane.fill': 'send',
'person.fill': 'person', // Add this
// ...
} as IconMapping;
Adding a Stack Screen
For full-screen pages outside the tab navigation:
Create the screen file
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
export default function SettingsScreen() {
return (
<ThemedView style={{ flex: 1, padding: 20 }}>
<ThemedText type="title">Settings</ThemedText>
</ThemedView>
);
}
Register in root layout
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
<Stack.Screen
name="settings"
options={{
title: 'Settings',
presentation: 'card',
}}
/>
</Stack>
Adding a Modal Screen
Modals present over the current screen:
<Stack.Screen
name="modal"
options={{
presentation: 'modal',
title: 'Modal',
headerShown: true,
}}
/>
Navigation Patterns
Navigating Between Screens
Link Component
useRouter Hook
Navigation with Params
import { Link } from 'expo-router';
<Link href="/settings">
<ThemedText type="link">Open Settings</ThemedText>
</Link>
import { useRouter } from 'expo-router';
function MyComponent() {
const router = useRouter();
const handlePress = () => {
router.push('/settings');
};
return (
<Button title="Settings" onPress={handlePress} />
);
}
import { Link } from 'expo-router';
<Link
href={{
pathname: '/user/[id]',
params: { id: '123' }
}}
>
<ThemedText>View User</ThemedText>
</Link>
Link Preview Feature
Expo Router includes advanced link features with previews:
import { Link } from 'expo-router';
<Link href="/modal">
<Link.Trigger>
<ThemedText type="subtitle">Open Modal</ThemedText>
</Link.Trigger>
<Link.Preview />
<Link.Menu>
<Link.MenuAction
title="Action"
icon="cube"
onPress={() => alert('Action pressed')}
/>
<Link.MenuAction
title="Share"
icon="square.and.arrow.up"
onPress={() => alert('Share pressed')}
/>
<Link.Menu title="More" icon="ellipsis">
<Link.MenuAction
title="Delete"
icon="trash"
destructive
onPress={() => alert('Delete pressed')}
/>
</Link.Menu>
</Link.Menu>
</Link>
This creates a long-press menu on iOS and a context menu on web.
Navigation Options
Screen Options
<Stack.Screen
name="modal"
options={{
presentation: 'modal', // or 'card', 'transparentModal'
}}
/>
<Stack.Screen
name="slide"
options={{
animation: 'slide_from_right', // or 'fade', 'flip', etc.
}}
/>
<Tabs.Screen
name="home"
options={{
tabBarLabel: 'Home',
tabBarBadge: 3,
tabBarBadgeStyle: { backgroundColor: 'red' },
}}
/>
Dynamic Options
Set options based on screen state:
import { Stack, useNavigation } from 'expo-router';
import { useLayoutEffect } from 'react';
export default function DetailsScreen() {
const navigation = useNavigation();
useLayoutEffect(() => {
navigation.setOptions({
title: 'Dynamic Title',
headerRight: () => (
<Button title="Save" onPress={() => {}} />
),
});
}, [navigation]);
return <View>{/* Content */}</View>;
}
Route Parameters
Creating Dynamic Routes
Use square brackets for dynamic segments:
src/app/
├── user/
│ └── [id].tsx → /user/:id
├── posts/
│ └── [slug].tsx → /posts/:slug
└── [...missing].tsx → Catch-all route
Accessing Parameters
import { useLocalSearchParams } from 'expo-router';
export default function UserScreen() {
const { id } = useLocalSearchParams();
return <ThemedText>User ID: {id}</ThemedText>;
}
Route Groups
Parentheses create route groups without affecting the URL:
src/app/
├── (tabs)/ → Grouped but not in URL
│ ├── _layout.tsx
│ ├── index.tsx → /
│ └── explore.tsx → /explore
└── (auth)/ → Another group
├── login.tsx → /login
└── register.tsx → /register
Route groups are useful for:
- Organizing related screens
- Applying shared layouts
- Keeping URLs clean
Deep Linking
Expo Router automatically handles deep links:
# Opens the explore tab
npx uri-scheme open nearyou://explore --ios
# Opens user profile with ID
npx uri-scheme open nearyou://user/123 --ios
Configure in app.json:
{
"expo": {
"scheme": "nearyou"
}
}
Best Practices
Flat Structure
Keep navigation hierarchy shallow (2-3 levels max) for better UX
Type Safety
Use TypeScript to catch navigation errors at compile time
Loading States
Show loading indicators during navigation transitions
Back Behavior
Ensure back navigation works intuitively on all platforms
Navigation API Reference
Router Methods
| Method | Description |
|---|
router.push(href) | Navigate to a new screen |
router.replace(href) | Replace current screen |
router.back() | Go back one screen |
router.canGoBack() | Check if back is possible |
router.setParams(params) | Update current params |
Navigation Hooks
| Hook | Purpose |
|---|
useRouter() | Access router methods |
usePathname() | Get current pathname |
useSegments() | Get URL segments array |
useLocalSearchParams() | Get current screen params |
useGlobalSearchParams() | Get all params in stack |
Next Steps
Components
Build UI with themed components
Expo Router Docs
Learn more about Expo Router