Creating a Screen
Screens in RNCopilot use expo-router , which maps files in the app/ directory to routes. This guide covers the three types of screens you will create: tab screens, stack screens, and auth screens.
Every screen file in app/ must use export default. This is an expo-router requirement. Named exports will not be recognized as routes.
Screen Types
| Type | Directory | Route Example | Use Case |
|---|---|---|---|
| Tab screen | app/(main)/(tabs)/<name>.tsx | Bottom tab navigation | Primary app sections |
| Stack screen | app/(main)/<name>.tsx | Push navigation | Detail pages, modals |
| Auth screen | app/(auth)/<name>.tsx | Auth flow | Login, register, forgot password |
Tab Screens
Tab screens appear in the bottom navigation bar. They are the primary entry points of the app.
Create the screen file
// app/(main)/(tabs)/products.tsx
import { useTranslation } from 'react-i18next';
import { View } from 'react-native';
import { StyleSheet } from 'react-native-unistyles';
import { ScreenContainer, Text } from '@/common/components';
export default function ProductsTab() {
const { t } = useTranslation();
return (
<ScreenContainer>
<View style={styles.container}>
<Text variant="h1">{t('products.title')}</Text>
</View>
</ScreenContainer>
);
}
const styles = StyleSheet.create((theme) => ({
container: {
flex: 1,
paddingTop: theme.metrics.spacingV.p16,
},
}));Key points:
ScreenContainerhandles safe area insets, background color, and optional scroll behavior- Pass
scrollabletoScreenContainerfor screens with scrolling content - Pass
paddedfor default horizontal padding - Use
StyleSheetfromreact-native-unistyles, not fromreact-native
Register the tab in the layout
Open app/(main)/(tabs)/_layout.tsx and add a Tabs.Screen entry. The name prop must match the filename (without .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="products"
options={{ title: t('tabs.products') }}
/>
<Tabs.Screen
name="settings"
options={{ title: t('tabs.settings') }}
/>
</Tabs>
);
}Add i18n keys
en.json
{
"tabs": {
"products": "Products"
},
"products": {
"title": "Products"
}
}Stack Screens
Stack screens are pushed onto the navigation stack from a tab or another stack screen. They appear with a back button and can be dismissed.
Create the screen file
Stack screens live directly in app/(main)/, outside the (tabs) group.
// app/(main)/product-details.tsx
import { useLocalSearchParams } from 'expo-router';
import { useTranslation } from 'react-i18next';
import { StyleSheet } from 'react-native-unistyles';
import { ScreenContainer, Text } from '@/common/components';
import { Loading } from '@/common/components/Loading';
import { useProduct } from '@/features/products/hooks/useProducts';
export default function ProductDetailsScreen() {
const { t } = useTranslation();
const { id } = useLocalSearchParams<{ id: string }>();
const { data: product, isLoading } = useProduct(id);
if (isLoading) return <Loading fullScreen />;
return (
<ScreenContainer scrollable padded>
<Text variant="h1">{product?.name}</Text>
<Text variant="body" style={styles.description}>
{product?.description}
</Text>
</ScreenContainer>
);
}
const styles = StyleSheet.create((theme) => ({
description: {
marginTop: theme.metrics.spacingV.p12,
color: theme.colors.text.secondary,
},
}));Navigate to the screen
Use router.push from expo-router to navigate:
import { router } from 'expo-router';
// From a product card or list item
<Card onPress={() => router.push(`/product-details?id=${product.id}`)}>Stack screens inside app/(main)/ are automatically part of the main stack navigator. You do not need to register them anywhere — expo-router picks them up from the file system.
Auth Screens
Auth screens live in a separate route group so they can have a different layout (no tabs, no header).
Create the screen file
// app/(auth)/login.tsx
import { useTranslation } from 'react-i18next';
import { View } from 'react-native';
import { StyleSheet } from 'react-native-unistyles';
import { ScreenContainer, Text, Button } from '@/common/components';
export default function LoginScreen() {
const { t } = useTranslation();
return (
<ScreenContainer scrollable padded>
<View style={styles.container}>
<Text variant="h1">{t('auth.loginTitle')}</Text>
{/* Login form goes here */}
</View>
</ScreenContainer>
);
}
const styles = StyleSheet.create((theme) => ({
container: {
flex: 1,
justifyContent: 'center',
gap: theme.metrics.spacingV.p16,
},
}));Protect routes
Use the useProtectedRoute hook to redirect unauthenticated users:
import { useProtectedRoute } from '@/hooks/useProtectedRoute';
export default function DashboardScreen() {
useProtectedRoute(); // Redirects to /(auth)/login if not authenticated
return (
<ScreenContainer>
{/* Protected content */}
</ScreenContainer>
);
}Screen Template Reference
Every screen should follow this template:
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 MyScreen() {
const { t } = useTranslation();
return (
<ScreenContainer scrollable padded>
<Text variant="h1">{t('myScreen.title')}</Text>
</ScreenContainer>
);
}
const styles = StyleSheet.create((theme) => ({
// styles here
}));Checklist
- File is in the correct directory (
(tabs)/,(main)/, or(auth)/) - Uses
export default(expo-router requirement) - Uses
ScreenContaineras root - All text uses
useTranslation()— no hardcoded strings - Styles use
StyleSheet.create((theme) => ({...}))fromreact-native-unistyles - Tab screens are registered in
app/(main)/(tabs)/_layout.tsx - i18n keys added to both
en.jsonandar.json