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!

typescript
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

typescript
import { Link } from 'expo-router';

function HomeScreen() {
  return (
    <Link href="/profile">
      Go to Profile
    </Link>
  );
}

Using Router Hooks

typescript
import { router } from 'expo-router';

function MyComponent() {
  const handleNavigate = () => {
    router.push('/profile');
  };

  return (
    <Button onPress={handleNavigate}>
      Go to Profile
    </Button>
  );
}

Navigation Methods

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

src/app/user/[id].tsx
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:

typescript
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     # /register
Route groups are perfect for organizing related screens while keeping URLs clean.

Layouts

Layouts wrap child routes and persist across navigation:

src/app/(tabs)/_layout.tsx
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

typescript
// 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

typescript
// 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:

src/app/profile.tsx
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:

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 screen
  • myapp://profile → Profile screen
  • myapp://user/123 → User screen with id=123

Navigation in ViewModels

Following the MVVM pattern, handle navigation in ViewModels:

src/presentation/viewmodels/useProfileViewModel.ts
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,
  };
}
Keep navigation logic in ViewModels to maintain separation of concerns.

Common Patterns

Protected Routes

typescript
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

src/app/_layout.tsx
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

typescript
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 detail

Best 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.

Next Steps