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
| File | Purpose |
|---|---|
_layout.tsx | Defines the navigator for that directory (Stack, Tabs, etc.) |
index.tsx | The default route for that directory |
+not-found.tsx | Catch-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
- 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>
);
}- 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} />
),
}}
/>- Add translation keys to both
en.jsonandar.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.
Navigation
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} />;
}Link Component
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: /loginThis 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:
| Alias | Resolves To | Usage |
|---|---|---|
@/* | 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.tsxThe useProtectedRoute hook (see Authentication) automatically redirects between (auth) and (main) based on session state.