Skip to main content
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:
cd my-first-app
code .
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

This file configures your app:
app.json
{
  "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
{
  "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
The root layout wraps all screens:
app/_layout.tsx
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
The main screen of your app:
app/index.tsx
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:
npx expo start
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

Using Your Phone

  1. Install Expo Go:
  2. Scan QR code:
    • iOS: Use Camera app
    • Android: Use Expo Go app
  3. 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:
npx expo start --tunnel

Step 5: Make Your First Change

With your app running, let’s make a change:
1

Open app/index.tsx

Open the file in your editor
2

Change the title

Find this line:
app/index.tsx
<ThemedText type="title" style={styles.title}>
  Welcome to Expo
</ThemedText>
Change it to:
app/index.tsx
<ThemedText type="title" style={styles.title}>
  Hello, I built this!
</ThemedText>
3

Save the file

Save (Cmd+S or Ctrl+S)
4

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.

Step 6: Add a Button

Let’s add some interactivity:
app/index.tsx
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:
app.json
{
  "expo": {
    "name": "My Awesome App",  // Change this
    "slug": "my-awesome-app"
  }
}

Change App Icon

  1. Create a 1024x1024px PNG image
  2. Save as assets/images/icon.png
  3. 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:
app/_layout.tsx
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

Try these solutions:
  1. Ensure same WiFi network
  2. Use tunnel mode: npx expo start --tunnel
  3. Check firewall settings
  4. Restart dev server
Clear the cache:
npx expo start --clear
Install types:
npm install --save-dev @types/react @types/react-native