Skip to main content
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
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:
src/app/_layout.tsx
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:
tabBarButton: HapticTab
See src/components/haptic-tab.tsx:1
Icons automatically use SF Symbols on iOS and Material Icons on Android/web:
tabBarIcon: ({ color }) => (
  <IconSymbol size={28} name="house.fill" color={color} />
)

Adding Screens

Adding a Tab Screen

1

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,
  },
});
2

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} />
    ),
  }}
/>
3

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:
1

Create the screen file

src/app/settings.tsx
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>
  );
}
2

Register in root layout

src/app/_layout.tsx
<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:
src/app/_layout.tsx
<Stack.Screen
  name="modal"
  options={{
    presentation: 'modal',
    title: 'Modal',
    headerShown: true,
  }}
/>
Expo Router includes advanced link features with previews:
src/app/(tabs)/index.tsx
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.

Screen Options

<Stack.Screen
  name="details"
  options={{
    headerShown: true,
    title: 'Details',
    headerBackTitle: 'Back',
    headerLargeTitle: true,
  }}
/>
<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:
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

Router Methods

MethodDescription
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
HookPurpose
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

Build docs developers (and LLMs) love