Skip to Content
GuidesCreating a Screen

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

TypeDirectoryRoute ExampleUse Case
Tab screenapp/(main)/(tabs)/<name>.tsxBottom tab navigationPrimary app sections
Stack screenapp/(main)/<name>.tsxPush navigationDetail pages, modals
Auth screenapp/(auth)/<name>.tsxAuth flowLogin, 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:

  • ScreenContainer handles safe area insets, background color, and optional scroll behavior
  • Pass scrollable to ScreenContainer for screens with scrolling content
  • Pass padded for default horizontal padding
  • Use StyleSheet from react-native-unistyles, not from react-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

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

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 ScreenContainer as root
  • All text uses useTranslation() — no hardcoded strings
  • Styles use StyleSheet.create((theme) => ({...})) from react-native-unistyles
  • Tab screens are registered in app/(main)/(tabs)/_layout.tsx
  • i18n keys added to both en.json and ar.json
Last updated on