Creating Screens and Routing
This skill guides the creation of screens and routing configuration using Expo Router and the project's screen/layout patterns.
Technologies & Stack
- Expo Router (file-based routing)
- React Native with TypeScript
- Stack and Tabs from
expo-router
- Safe Area via
react-native-safe-area-context
- Theme via
useAppTheme and useStyles
- Core components from
src/ui/components/core/ (Screen, Text, Button, Icon, etc.)
- Icons: use only the core
Icon component. Do not use @expo/vector-icons, Ionicons, or any other icon library directly.
Concepts
Route file vs Screen component
- Route file (
src/app/...): Thin entry point that only imports and renders the screen component. No layout/UI logic here.
- Screen component (
src/ui/screens/...): Actual UI, hooks, and styles. Reusable and testable.
Route groups
Parentheses create groups (no segment in URL): (application), (tabs).
- Use for layout nesting without changing the URL.
- Example:
(application)/(tabs)/home → URL /home, not /(application)/(tabs)/home.
App structure (src/app)
app/
├── _layout.tsx # Root layout: ThemeProvider, SafeAreaProvider, AuthProvider, Stack
├── index.tsx # Entry route (e.g. onboarding) → renders OnboardingScreen
└── (application)/ # Route group (protected)
├── _layout.tsx # Protected layout: redirect if not auth, Stack with (tabs)
└── (tabs)/ # Route group for tab navigation
├── _layout.tsx # Tabs layout
└── home.tsx # Tab screen
Route file pattern
Route files are minimal: default export that renders the screen component.
tsx
1// src/app/(application)/(tabs)/home.tsx
2import { HomeScreen } from "../../../ui/screens/Home/Home"
3
4export default function Home() {
5 return <HomeScreen />
6}
Rules:
- One default export (the page component).
- No business logic or layout in the route file; delegate to the screen.
Screen component structure (src/ui/screens)
Each feature screen lives in its own folder:
ScreenName/
├── ScreenName.tsx # Main screen component
├── styles.ts # Theme-based styles (stylesTheme)
├── useScreenName.ts # Hook: actions, states, refs (UI only)
├── components/ # Optional: screen-specific components
│ └── Header/
│ ├── Header.tsx
│ ├── styles.ts
│ └── index.ts
└── __tests__/ # Optional
└── ScreenName.test.tsx
A screen folder may include a components/ subfolder for components used only by that screen. If a component is reused across screens, place it in src/ui/components/core/ or a shared components folder instead.
Screen-specific components (components/)
Components in components/ follow this structure:
ScreenName/components/ComponentName/
├── ComponentName.tsx
├── styles.ts # Optional: stylesTheme(theme) when component has its own styles
└── index.ts # export { ComponentName } from "./ComponentName"
Patterns:
- Props receive data and callbacks from the parent screen (e.g.
userName, onNotificationsPress). The screen hook provides states and actions; pass them down as props.
- Use core components:
Text, Button, Icon (never Ionicons or other icon libs directly).
- Use
useAppTheme() and stylesTheme(theme) in component styles when the component has its own styles.
- Use theme tokens (
theme.action["brand-primary"], spacings, radius) in styles.
- Component styles file:
styles.ts in the component folder, exporting stylesTheme(theme: ThemeType).
Example: Header component (Home screen)
Home/components/Header/
├── Header.tsx
├── styles.ts
└── index.ts
tsx
1// Header.tsx
2import { Image, View } from "react-native"
3import { Text } from "../../../../components/core/Text/Text"
4import { Icon } from "../../../../components/core/Icon"
5import { useAppTheme } from "../../../../theme/hooks/useAppTheme"
6import { stylesTheme } from "./styles"
7
8type HeaderProps = {
9 userName: string
10 onNotificationsPress: () => void
11}
12
13export function Header({ userName, onNotificationsPress }: HeaderProps) {
14 const { theme } = useAppTheme()
15 const styles = stylesTheme(theme)
16
17 return (
18 <View style={styles.row}>
19 <View style={styles.leftContent}>
20 <View style={styles.avatarContainer}>
21 <Image source={{ uri: "..." }} style={styles.avatar} />
22 </View>
23 <View style={styles.textContainer}>
24 <Text variant="title-large-bold">Olá, {userName}!</Text>
25 <Text variant="title-small-reg">Vamos treinar hoje?</Text>
26 </View>
27 </View>
28 <Icon
29 onPress={onNotificationsPress}
30 pressableStyle={styles.bellButton}
31 name="icon-notification"
32 size={21}
33 variant="default"
34 />
35 </View>
36 )
37}
typescript
1// styles.ts
2import { StyleSheet } from "react-native"
3import { ThemeType } from "../../../../theme"
4import { radius } from "../../../../theme/tokens/sizes"
5import { spacings } from "../../../../theme/tokens/spacings"
6
7const AVATAR_SIZE = 48
8
9export const stylesTheme = (theme: ThemeType) =>
10 StyleSheet.create({
11 row: {
12 flexDirection: "row",
13 alignItems: "center",
14 justifyContent: "space-between",
15 gap: spacings.padding[8],
16 },
17 avatarContainer: {
18 width: AVATAR_SIZE,
19 height: AVATAR_SIZE,
20 borderRadius: AVATAR_SIZE / 2,
21 overflow: "hidden",
22 backgroundColor: theme.action["brand-primary"],
23 },
24 avatar: {
25 width: AVATAR_SIZE,
26 height: AVATAR_SIZE,
27 },
28 })
Usage in screen:
tsx
1// Home.tsx
2<Header
3 userName={states.userName}
4 onNotificationsPress={actions.handleNotificationsPress}
5/>
Screen component
- Always use the core
Screen component as the main container of the screen. No other root wrapper (e.g. plain View) for the whole screen.
- Receives no route params in the component signature unless the route passes them.
- Uses a custom hook that returns
actions, states, and refs.
- Uses
useStyles with a stylesTheme(theme, ...) function for styles.
- Prefer core components:
Screen, Text, Button, Icon, etc. Icons must use the core Icon component only—no Ionicons, @expo/vector-icons, or other icon libs.
tsx
1// ScreenName.tsx
2import { View } from "react-native"
3import { Text } from "../../components/core/Text/Text"
4import { Screen } from "../../components/core/Screen/Screen"
5import { useStyles } from "../../theme/hooks/useStyles"
6import { stylesTheme } from "./styles"
7import { useScreenName } from "./useScreenName"
8
9export function ScreenNameScreen() {
10 const { actions, states, refs } = useScreenName()
11 const styles = useStyles((theme) => stylesTheme(theme, states.insets))
12
13 return (
14 <Screen>
15 <View style={styles.container} ref={refs.containerRef}>
16 <Text variant="heading">Title</Text>
17 {/* ... */}
18 </View>
19 </Screen>
20 )
21}
styles.ts
- Export a function
stylesTheme(theme: ThemeType, ...deps) that returns a StyleSheet.create(...).
- Use theme tokens:
theme.surface.background, spacings, etc.
- Use safe area insets when needed (e.g. bottom padding).
typescript
1// styles.ts
2import { StyleSheet } from "react-native"
3import { ThemeType } from "../../theme"
4import { spacings } from "../../theme/tokens/spacings"
5import { EdgeInsets } from "react-native-safe-area-context"
6
7export const stylesTheme = (theme: ThemeType, insets: EdgeInsets) =>
8 StyleSheet.create({
9 container: {
10 flex: 1,
11 gap: spacings.gap[32],
12 paddingBottom:
13 Math.max(insets.bottom, spacings.padding[20]) + spacings.padding[20],
14 },
15 })
useScreenName hook
- Return always
{ actions, states, refs }. Even if one category is empty, expose the key (e.g. refs: {}).
- UI rules only: no business logic. The hook handles only UI concerns: navigation (e.g.
useRouter), safe area, theme, refs, local UI state (e.g. focus, visibility). Business rules (validation, API calls, domain logic) belong in domain/use cases, not in the screen hook.
actions: handlers for user interactions (e.g. navigate, toggle, submit that delegates to a service).
states: derived or reactive values for rendering (e.g. insets, theme, loading flags coming from props/context).
refs: refs for DOM/native elements used by the screen (e.g. scroll ref, input ref). Use useRef and expose them in refs.
typescript
1// useScreenName.ts
2import { useRef } from "react"
3import { View } from "react-native"
4import { useSafeAreaInsets } from "react-native-safe-area-context"
5import { useRouter } from "expo-router"
6import { useAppTheme } from "../../theme/hooks/useAppTheme"
7
8export const useScreenName = () => {
9 const router = useRouter()
10 const insets = useSafeAreaInsets()
11 const { theme } = useAppTheme()
12 const containerRef = useRef<View>(null)
13
14 const handleNavigate = () => {
15 router.navigate("/other-route")
16 }
17
18 return {
19 actions: { handleNavigate },
20 states: { insets, theme },
21 refs: { containerRef },
22 }
23}
Root layout (_layout.tsx)
- Wraps app with:
ThemeProvider → SafeAreaProvider → AuthProvider → routing.
- Uses
Stack from expo-router.
- Uses
Stack.Protected with guard={!!auth?.id} for protected routes.
- Entry route:
name="index" with headerShown: false.
tsx
1const Routes = () => {
2 const { auth } = useAuth()
3 return (
4 <Stack>
5 <Stack.Screen name="index" options={{ headerShown: false }} />
6 <Stack.Protected guard={!!auth?.id}>
7 <Stack.Screen name="(application)" />
8 </Stack.Protected>
9 </Stack>
10 )
11}
Protected layout (e.g. (application)/_layout.tsx)
- Redirect to entry if not authenticated.
- Use
Stack with screenOptions: headerShown: false, fullScreenGestureEnabled: true as in the project.
tsx
1import { Redirect, Stack } from "expo-router"
2import { useAuth } from "../../domain/auth/AuthContext"
3
4export default function ProtectedLayout() {
5 const { auth } = useAuth()
6 if (!auth?.id) return <Redirect href="/" />
7 return (
8 <Stack screenOptions={{ headerShown: false, fullScreenGestureEnabled: true }}>
9 <Stack.Screen name="(tabs)" />
10 </Stack>
11 )
12}
Tabs layout
- Use
Tabs from expo-router.
- One
Tabs.Screen per tab file (e.g. home, profile).
tsx
1import { Tabs } from "expo-router"
2
3export default function TabLayout() {
4 return (
5 <Tabs>
6 <Tabs.Screen name="home" />
7 <Tabs.Screen name="profile" />
8 </Tabs>
9 )
10}
Adding a new screen (step-by-step)
1. Screen folder and files
- Create
src/ui/screens/FeatureName/FeatureName.tsx (export FeatureNameScreen).
- Create
src/ui/screens/FeatureName/styles.ts with stylesTheme(theme, ...).
- Create
src/ui/screens/FeatureName/useFeatureName.ts with useFeatureName() returning { actions, states, refs } (UI-only, no business logic).
2. Route file
- Create the route file in
src/app/ according to desired URL:
- Tab:
(application)/(tabs)/featureName.tsx
- Stack inside app:
(application)/featureName.tsx
- Entry-level: e.g.
someRoute.tsx and register in root _layout.tsx if needed.
- In the route file: import the screen and default-export a function that renders it.
3. Register in layout (if needed)
- New tab: add
<Tabs.Screen name="featureName" /> in (application)/(tabs)/_layout.tsx.
- New stack screen: add
<Stack.Screen name="featureName" /> in the corresponding _layout.tsx.
Navigation
- Use
useRouter() from expo-router: router.push(href), router.replace(href), router.back().
- Use path strings:
"/", "/home", "/(application)/(tabs)/profile" (group names don’t appear in URL).
- Use
<Link href="..."> for declarative navigation when appropriate.
Naming conventions
- Route files: camelCase, e.g.
home.tsx, featureName.tsx.
- Screen folder: PascalCase, e.g.
Onboarding, Home, FeatureName.
- Screen component:
ScreenNameScreen (e.g. OnboardingScreen, HomeScreen).
- Hook:
useScreenName (e.g. useOnboarding, useHome).
- Styles:
stylesTheme in styles.ts.
Checklist
When creating a new screen and route: