Skip to Content
Core ConceptsRouting (Expo Router)

Routing (Expo Router)

RNCopilot uses expo-router for file-based, typed routing. Every file in the app/ directory automatically becomes a route — no manual route registration required.

Directory Structure

app/ _layout.tsx -- Root layout (providers, ErrorBoundary) +not-found.tsx -- 404 screen (main)/ _layout.tsx -- Main stack navigator (tabs)/ _layout.tsx -- Tab navigator index.tsx -- Home tab (/) settings.tsx -- Settings tab (/settings)

Key Conventions

FilePurpose
_layout.tsxDefines the navigator for that directory (Stack, Tabs, etc.)
index.tsxThe default route for that directory
+not-found.tsxCatch-all 404 screen
(group)/Route group — wraps children in a shared layout without adding a URL segment

Tab Navigation

The tab layout is defined in app/(main)/(tabs)/_layout.tsx:

// app/(main)/(tabs)/_layout.tsx import { Tabs } from 'expo-router'; import { useTranslation } from 'react-i18next'; import { TabBar } from '@/common/components/TabBar'; export default function TabLayout() { const { t } = useTranslation(); return ( <Tabs tabBar={(props) => <TabBar {...props} />} screenOptions={{ headerShown: false }} > <Tabs.Screen name="index" options={{ title: t('tabs.home') }} /> <Tabs.Screen name="settings" options={{ title: t('tabs.settings') }} /> </Tabs> ); }

Adding a New Tab

  1. Create the screen file:
// app/(main)/(tabs)/explore.tsx import { useTranslation } from 'react-i18next'; import { ScreenContainer } from '@/common/components/ScreenContainer'; import { Text } from '@/common/components/Text'; export default function ExploreTab() { const { t } = useTranslation(); return ( <ScreenContainer> <Text variant="h1">{t('explore.title')}</Text> </ScreenContainer> ); }
  1. Register it in the tab layout:
// app/(main)/(tabs)/_layout.tsx <Tabs.Screen name="explore" options={{ title: t('tabs.explore'), tabBarIcon: ({ color, size }) => ( <Icon name="compass-outline" color={color} size={size} /> ), }} />
  1. Add translation keys to both en.json and ar.json:
{ "tabs": { "explore": "Explore" }, "explore": { "title": "Explore" } }

Screen Files

Every screen file in app/ must use a default export. This is an Expo Router requirement.

// app/(main)/(tabs)/index.tsx import { useTranslation } from 'react-i18next'; import { StyleSheet } from 'react-native-unistyles'; import { ScreenContainer } from '@/common/components/ScreenContainer'; import { Text } from '@/common/components/Text'; export default function HomeScreen() { const { t } = useTranslation(); return ( <ScreenContainer scrollable> <Text variant="h1">{t('home.welcome')}</Text> </ScreenContainer> ); } const styles = StyleSheet.create((theme) => ({ // ... }));

Default exports are only for screen files in app/. Components in src/ always use named exports.

Programmatic Navigation

import { router } from 'expo-router'; // Push a new screen onto the stack router.push('/products/123'); // Replace the current screen (no back button) router.replace('/(auth)/login'); // Go back router.back();

Using the Hook

import { useRouter } from 'expo-router'; function MyComponent() { const router = useRouter(); const handlePress = () => { router.push('/products'); }; return <Button title="Browse" onPress={handlePress} />; }
import { Link } from 'expo-router'; <Link href="/settings"> <Text>Go to Settings</Text> </Link>

Typed Routes

Expo Router generates types for all routes in your project. You get autocomplete and type checking for route paths:

// TypeScript will validate this path exists router.push('/(main)/(tabs)/settings'); // This will show a type error if the route doesn't exist router.push('/nonexistent-route'); // Error!

Route Groups

Parenthesized directories like (main) and (tabs) create route groups. They provide shared layouts without adding segments to the URL:

app/(main)/(tabs)/settings.tsx --> URL: /settings app/(auth)/login.tsx --> URL: /login

This is useful for:

  • Wrapping tab screens in a tab navigator without /tabs/ in the URL
  • Separating authenticated and unauthenticated route groups
  • Applying different layouts to groups of screens

Path Aliases

The project configures two path aliases to eliminate deep relative imports:

AliasResolves ToUsage
@/*src/*import { Button } from '@/common/components/Button'
~/*app/*import type { ScreenProps } from '~/types'
// CORRECT import { Button } from '@/common/components/Button'; import { useAuthStore } from '@/providers/auth/authStore'; // WRONG -- climbing relative paths import { Button } from '../../../common/components/Button';

Path aliases are configured in both tsconfig.json (for TypeScript) and babel.config.js (for Metro bundler). Both must agree for imports to resolve correctly.

Adding Non-Tab Screens

For screens that are not tabs (e.g., a detail screen pushed from a tab), add them to the (main) group:

// app/(main)/product-detail.tsx export default function ProductDetailScreen() { // This screen slides in from the right, with a back button // ... }

Navigate to it:

router.push('/(main)/product-detail');

Auth Route Group

To add protected auth screens (login, register, forgot password), create an (auth) group:

app/ (auth)/ _layout.tsx -- Auth stack layout login.tsx register.tsx forgot-password.tsx

The useProtectedRoute hook (see Authentication) automatically redirects between (auth) and (main) based on session state.

Last updated on