In this tutorial, you’ll create your first Expo app, explore the project structure, run it on your device, and make your first changes. By the end, you’ll understand the foundation of every Expo app.
What You’ll Build
A simple “Hello World” app that:
Displays a welcome screen
Uses Expo Router for navigation
Runs on iOS, Android, and web
Updates instantly with Fast Refresh
Time required: 10-15 minutes
Prerequisites
Before starting, make sure you have:
Node.js 18+ installed
A code editor (VS Code recommended)
A phone with Expo Go installed OR iOS Simulator / Android Emulator
If you need help with installation, see the Installation Guide .
Step 1: Create the Project
Open your terminal and run:
npx create-expo-app my-first-app
You’ll see output like this:
✔ Downloaded and extracted project files.
✔ Installed JavaScript dependencies.
✓ Your project is ready!
To run your project, navigate to the directory and run one of the following commands:
- cd my-first-app
- npx expo start
This creates a new Expo project with Expo Router pre-configured for file-based routing.
Step 2: Explore the Project Structure
Navigate into your project and open it in your editor:
Let’s understand the structure:
my-first-app/
├── app/ # Your app screens (file-based routing)
│ ├── _layout.tsx # Root layout
│ ├── index.tsx # Home screen (route: /)
│ └── explore.tsx # Explore screen (route: /explore)
│
├── assets/ # Images, fonts, and other static files
│ ├── fonts/
│ └── images/
│
├── components/ # Reusable React components
│ ├── themed-text.tsx
│ └── themed-view.tsx
│
├── constants/ # App-wide constants (colors, spacing)
│ └── theme.ts
│
├── app.json # Expo configuration
├── package.json # Dependencies and scripts
├── tsconfig.json # TypeScript configuration
└── README.md
Key Files Explained
app.json - App Configuration
This file configures your app: {
"expo" : {
"name" : "my-first-app" ,
"slug" : "my-first-app" ,
"version" : "1.0.0" ,
"orientation" : "portrait" ,
"icon" : "./assets/images/icon.png" ,
"scheme" : "myapp" ,
"userInterfaceStyle" : "automatic" ,
"splash" : {
"image" : "./assets/images/splash.png" ,
"resizeMode" : "contain" ,
"backgroundColor" : "#ffffff"
},
"ios" : {
"supportsTablet" : true ,
"bundleIdentifier" : "com.yourcompany.myfirstapp"
},
"android" : {
"adaptiveIcon" : {
"foregroundImage" : "./assets/images/adaptive-icon.png" ,
"backgroundColor" : "#ffffff"
},
"package" : "com.yourcompany.myfirstapp"
},
"web" : {
"bundler" : "metro" ,
"output" : "static" ,
"favicon" : "./assets/images/favicon.png"
}
}
}
Key settings:
name: App display name
slug: URL-friendly identifier
version: App version
icon: App icon (1024x1024px)
scheme: Deep linking URL scheme
bundleIdentifier (iOS) / package (Android): Unique app identifier
package.json - Dependencies
{
"name" : "my-first-app" ,
"main" : "expo-router/entry" ,
"version" : "1.0.0" ,
"scripts" : {
"start" : "expo start" ,
"reset-project" : "node ./scripts/reset-project.js" ,
"android" : "expo start --android" ,
"ios" : "expo start --ios" ,
"web" : "expo start --web" ,
"lint" : "expo lint"
},
"dependencies" : {
"expo" : "~55.0.0" ,
"expo-router" : "~55.0.0" ,
"expo-status-bar" : "~55.0.0" ,
"react" : "19.2.0" ,
"react-native" : "0.83.2" ,
"react-native-safe-area-context" : "~5.6.2" ,
"react-native-screens" : "~4.23.0"
},
"devDependencies" : {
"@types/react" : "~19.2.2" ,
"typescript" : "~5.9.2"
}
}
Important:
main: Entry point (Expo Router)
scripts: Common commands
dependencies: Runtime libraries
app/_layout.tsx - Root Layout
The root layout wraps all screens: import { DarkTheme , DefaultTheme , ThemeProvider } from '@react-navigation/native' ;
import React from 'react' ;
import { useColorScheme } from 'react-native' ;
import { AnimatedSplashOverlay } from '@/components/animated-icon' ;
import AppTabs from '@/components/app-tabs' ;
export default function TabLayout () {
const colorScheme = useColorScheme ();
return (
< ThemeProvider value = { colorScheme === 'dark' ? DarkTheme : DefaultTheme } >
< AnimatedSplashOverlay />
< AppTabs />
</ ThemeProvider >
);
}
This:
Detects light/dark mode
Provides theme to all screens
Shows animated splash screen
Sets up tab navigation
app/index.tsx - Home Screen
The main screen of your app: import * as Device from 'expo-device' ;
import { Platform , StyleSheet } from 'react-native' ;
import { SafeAreaView } from 'react-native-safe-area-context' ;
import { AnimatedIcon } from '@/components/animated-icon' ;
import { ThemedText } from '@/components/themed-text' ;
import { ThemedView } from '@/components/themed-view' ;
export default function HomeScreen () {
return (
< ThemedView style = { styles . container } >
< SafeAreaView style = { styles . safeArea } >
< ThemedView style = { styles . heroSection } >
< AnimatedIcon />
< ThemedText type = "title" style = { styles . title } >
Welcome to Expo
</ ThemedText >
</ ThemedView >
</ SafeAreaView >
</ ThemedView >
);
}
const styles = StyleSheet . create ({
container: {
flex: 1 ,
justifyContent: 'center' ,
flexDirection: 'row' ,
},
safeArea: {
flex: 1 ,
paddingHorizontal: 16 ,
alignItems: 'center' ,
},
heroSection: {
alignItems: 'center' ,
justifyContent: 'center' ,
flex: 1 ,
},
title: {
textAlign: 'center' ,
},
});
Step 3: Start the Development Server
Run the development server:
You’ll see:
› Metro waiting on exp://192.168.1.100:8081
› Scan the QR code above with Expo Go (Android) or the Camera app ( iOS )
› Press a │ open Android
› Press i │ open iOS simulator
› Press w │ open web
› Press r │ reload app
› Press m │ toggle menu
› Press o │ open project code in your editor
The server will continue running. Leave this terminal open and use a new terminal for other commands.
Step 4: Run on Your Device
Physical Device
iOS Simulator
Android Emulator
Web Browser
Using Your Phone
Install Expo Go:
Scan QR code:
iOS: Use Camera app
Android: Use Expo Go app
App loads:
Your app opens in Expo Go!
Your phone and computer must be on the same WiFi network. If scanning doesn’t work, try: Using iOS Simulator (Mac only) Press i in the terminal where expo start is running. Expo will:
Open Xcode Simulator
Install Expo Go automatically
Launch your app
If you don’t have Xcode installed, download it from the Mac App Store (free, but large download).
Using Android Emulator
Start an emulator in Android Studio
Press a in the terminal
Expo will:
Detect the running emulator
Install Expo Go
Launch your app
Create one in Android Studio:
Open Android Studio
Tools → Device Manager
Create Device
Select Pixel 6 (or any device)
Download system image (e.g., Android 14)
Finish and start the emulator
Using Web Browser Press w in the terminal. Your default browser opens with your app running! Web support is built-in with Metro bundler. No additional configuration needed.
Step 5: Make Your First Change
With your app running, let’s make a change:
Open app/index.tsx
Open the file in your editor
Change the title
Find this line: < ThemedText type = "title" style = { styles . title } >
Welcome to Expo
</ ThemedText >
Change it to: < ThemedText type = "title" style = { styles . title } >
Hello, I built this!
</ ThemedText >
Save the file
Save (Cmd+S or Ctrl+S)
See instant update
Watch your app update automatically without losing any state!
This is Fast Refresh in action - one of Expo’s most powerful development features.
Let’s add some interactivity:
import { useState } from 'react' ;
import { Button , StyleSheet } from 'react-native' ;
import { SafeAreaView } from 'react-native-safe-area-context' ;
import { ThemedText } from '@/components/themed-text' ;
import { ThemedView } from '@/components/themed-view' ;
export default function HomeScreen () {
const [ count , setCount ] = useState ( 0 );
return (
< ThemedView style = { styles . container } >
< SafeAreaView style = { styles . safeArea } >
< ThemedView style = { styles . content } >
< ThemedText type = "title" style = { styles . title } >
Hello, I built this!
</ ThemedText >
< ThemedView style = { styles . counterSection } >
< ThemedText type = "subtitle" >
You pressed the button { count } times
</ ThemedText >
< Button
title = "Press me!"
onPress = { () => setCount ( count + 1 ) }
/>
</ ThemedView >
</ ThemedView >
</ SafeAreaView >
</ ThemedView >
);
}
const styles = StyleSheet . create ({
container: {
flex: 1 ,
},
safeArea: {
flex: 1 ,
padding: 16 ,
},
content: {
flex: 1 ,
alignItems: 'center' ,
justifyContent: 'center' ,
gap: 32 ,
},
title: {
textAlign: 'center' ,
},
counterSection: {
gap: 16 ,
alignItems: 'center' ,
},
});
Save and watch the button appear instantly!
Notice how the count state persists when you make changes? That’s Fast Refresh preserving your component state.
Understanding What You Built
File-Based Routing
Your project uses Expo Router for navigation. Files in the app/ directory automatically become routes:
app/
index.tsx # Route: /
explore.tsx # Route: /explore
profile.tsx # Route: /profile (if you create it)
No need to manually configure routes!
Themed Components
The template includes themed components that automatically adapt to light/dark mode:
<ThemedView> - View that changes background
<ThemedText> - Text that changes color
Check components/ to see how they work.
Safe Areas
SafeAreaView ensures content doesn’t overlap with:
iPhone notch
Android navigation buttons
Status bars
Always wrap your screen content in SafeAreaView.
Common Mistakes to Avoid
Without SafeAreaView, content can be hidden behind the notch or status bar: // ❌ Bad
export default function Screen () {
return (
< View >
< Text > This might be hidden! </ Text >
</ View >
);
}
// ✓ Good
import { SafeAreaView } from 'react-native-safe-area-context' ;
export default function Screen () {
return (
< SafeAreaView >
< Text > This is visible! </ Text >
</ SafeAreaView >
);
}
React Native uses Flexbox for layout. Always set flex: 1 on containers: // ❌ Bad - content might not fill screen
const styles = StyleSheet . create ({
container: {
// Missing flex: 1
}
});
// ✓ Good
const styles = StyleSheet . create ({
container: {
flex: 1 ,
}
});
Keep styles in StyleSheet.create() for better performance: // ❌ Bad - inline styles
< View style = { { padding: 20 , margin: 10 } } >
< Text style = { { fontSize: 24 , color: 'blue' } } > Hello </ Text >
</ View >
// ✓ Good - StyleSheet
const styles = StyleSheet . create ({
container: {
padding: 20 ,
margin: 10 ,
},
text: {
fontSize: 24 ,
color: 'blue' ,
},
});
< View style = { styles . container } >
< Text style = { styles . text } > Hello </ Text >
</ View >
Project Customization
Change App Name
Edit app.json:
{
"expo" : {
"name" : "My Awesome App" , // Change this
"slug" : "my-awesome-app"
}
}
Change App Icon
Create a 1024x1024px PNG image
Save as assets/images/icon.png
It’s already configured in app.json:
{
"expo" : {
"icon" : "./assets/images/icon.png"
}
}
Add Custom Fonts
The template already loads fonts in app/_layout.tsx. To add more:
import { useFonts } from 'expo-font' ;
const [ fontsLoaded ] = useFonts ({
'Inter-Black' : require ( '../assets/fonts/Inter-Black.otf' ),
'MyFont-Regular' : require ( '../assets/fonts/MyFont-Regular.ttf' ),
});
Next Steps
Congratulations! You’ve created your first Expo app and learned:
Project structure
File-based routing
Fast Refresh
Running on multiple platforms
Making changes and seeing updates
Add Navigation Learn to navigate between screens
Core Concepts Understand how Expo works
SDK Modules Explore camera, location, and more
Build & Deploy Ship your app to production
Troubleshooting
Can't connect to dev server
Try these solutions:
Ensure same WiFi network
Use tunnel mode: npx expo start --tunnel
Check firewall settings
Restart dev server
Install types: npm install --save-dev @types/react @types/react-native