Skip to main content
Expo provides a streamlined development workflow with fast iteration, instant previews, and powerful debugging tools. This guide covers the complete development cycle from writing code to seeing it run on devices.

Development Cycle Overview

Starting Development

Launch Dev Server

Start the development server with:
npx expo start

What Happens When You Start

1

Metro Bundler Starts

Metro bundler launches on port 8081, ready to transform and bundle your JavaScript
2

Dev Server Starts

HTTP server starts to serve:
  • App manifest (configuration)
  • JavaScript bundles
  • Source maps
  • Assets (images, fonts)
3

QR Code Displayed

Terminal shows QR code and connection options:
 Metro waiting on exp://192.168.1.100:8081
 Scan the QR code above

 Press a open Android
 Press i open iOS simulator  
 Press w open web
4

Ready for Connections

Devices can now connect via:
  • QR code scan
  • Direct URL entry
  • Automatic launch (simulators)

Fast Refresh

Fast Refresh updates your app instantly when you save files, preserving component state.

How Fast Refresh Works

1

File Changes Detected

Watchman detects file modifications
2

Module Transformed

Metro transforms only the changed module
3

Hot Update Sent

Update is sent via WebSocket to connected devices
4

Component Re-renders

React re-renders the component, preserving state

What Gets Preserved

Preserved during Fast Refresh:
  • Component state (useState, useReducer)
  • Navigation state
  • Form inputs
  • Scroll position
Triggers Full Reload:
  • Changes to files outside app/ or src/
  • Native module changes
  • app.json modifications
  • New dependencies installed

Example: Fast Refresh in Action

app/index.tsx
import { useState } from 'react';
import { View, Text, Button } from 'react-native';

export default function HomeScreen() {
  const [count, setCount] = useState(0);

  return (
    <View>
      <Text>Count: {count}</Text>
      <Button title="Increment" onPress={() => setCount(count + 1)} />
      
      {/* Change this text and save - count state is preserved! */}
      <Text>Hello from Expo</Text>
    </View>
  );
}
When you edit the <Text> content and save, the change appears instantly without resetting count.

Development Server Features

Interactive Terminal

While the dev server is running, you can:
KeyAction
aOpen on Android emulator/device
iOpen on iOS simulator
wOpen in web browser
rReload app
mToggle developer menu
cShow project QR code
jOpen React DevTools (web)
oOpen project in editor

Developer Menu

Access the developer menu:
  • iOS Simulator: Cmd+D
  • Android Emulator: Cmd+M (Mac) or Ctrl+M (Windows/Linux)
  • Physical Device: Shake the device
  • Terminal: Press m
Menu Options:
  • Reload
  • Debug Remote JS (legacy)
  • Enable Fast Refresh
  • Toggle Element Inspector
  • Toggle Performance Monitor
  • Open React DevTools

Element Inspector

Inspect UI elements in your app:
  1. Open developer menu
  2. Select “Toggle Element Inspector”
  3. Tap any element to see its properties
View > View > Text
Props: style, children
Position: x: 20, y: 100
Size: 200 x 50

Performance Monitor

Monitor app performance:
  1. Open developer menu
  2. Select “Toggle Performance Monitor”
  3. View real-time metrics:
    • JS thread FPS
    • UI thread FPS
    • RAM usage
    • Bridge traffic

Metro Bundler

Metro is the JavaScript bundler that powers Expo development.

Metro Configuration

Customize Metro in metro.config.js:
metro.config.js
const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

// Add support for additional file extensions
config.resolver.assetExts.push('db');

// Add custom source extensions
config.resolver.sourceExts.push('mjs');

// Configure transformer
config.transformer.minifierPath = 'metro-minify-terser';

module.exports = config;

Metro Commands

npx expo start --clear

Asset Handling

Metro automatically handles: Images:
import logo from './assets/logo.png';

<Image source={logo} />
Fonts:
import { useFonts } from 'expo-font';

const [fontsLoaded] = useFonts({
  'Inter-Black': require('./assets/fonts/Inter-Black.otf'),
});
Other Assets:
import data from './assets/data.json';
import doc from './assets/document.pdf';

Build Types

Development Builds

Development builds include your exact dependencies and allow for custom native code. When to use:
  • Need custom native modules
  • Using libraries not in Expo Go
  • Configuring native project settings
  • Testing production-like behavior
Creating a development build:
# Local build
npx expo prebuild
npx expo run:ios
npx expo run:android

# Cloud build with EAS
eas build --profile development

Expo Go Builds

Expo Go is a pre-built app with common SDK modules. When to use:
  • Learning Expo
  • Prototyping quickly
  • Only using Expo SDK modules
  • Sharing demos
Limitations:
  • No custom native code
  • Limited to included SDK modules
  • Some libraries incompatible
Expo Go includes these SDK modules:
  • Camera, Location, Sensors
  • FileSystem, SQLite, SecureStore
  • Notifications, Updates
  • Image Picker, Document Picker
  • Video, Audio, AV
  • And 40+ more…
See full list.

Production Builds

Production builds are optimized for app stores.
# Export for production
npx expo export --platform ios
npx expo export --platform android

# Or use EAS Build
eas build --platform ios --profile production
eas build --platform android --profile production
Optimizations applied:
  • Code minification
  • Dead code elimination
  • Asset optimization
  • Source map generation
  • Bundle splitting

Debugging

React DevTools

Debug React components:
# Start dev server
npx expo start

# Press 'j' to open React DevTools
Features:
  • Component tree inspection
  • Props and state viewer
  • Performance profiling
  • Hook debugging

Console Logs

View console output in terminal:
console.log('Debug info:', data);
console.warn('Warning message');
console.error('Error details');
Logs appear in:
  • Terminal (where expo start is running)
  • React DevTools console
  • Browser console (web)

Debugging Native Code

iOS (Xcode):
  1. Open ios/YourApp.xcworkspace in Xcode
  2. Set breakpoints in Swift/Objective-C code
  3. Run from Xcode with debugging
Android (Android Studio):
  1. Open android/ folder in Android Studio
  2. Set breakpoints in Kotlin/Java code
  3. Run with debugger attached

Network Debugging

Inspect network requests:
import { Platform } from 'react-native';

if (__DEV__) {
  // Enable network inspector
  global.XMLHttpRequest = global.originalXMLHttpRequest || global.XMLHttpRequest;
}
Use React Native Debugger or Flipper for advanced network inspection.

Environment Configuration

Environment Variables

Use expo-constants for environment-specific config:
app.config.js
export default {
  expo: {
    extra: {
      apiUrl: process.env.API_URL || 'https://api.example.com',
      apiKey: process.env.API_KEY,
    },
  },
};
import Constants from 'expo-constants';

const apiUrl = Constants.expoConfig?.extra?.apiUrl;
const apiKey = Constants.expoConfig?.extra?.apiKey;

Development vs Production

Check environment:
import { Platform } from 'react-native';

if (__DEV__) {
  // Development only
  console.log('Running in development');
}

const API_URL = __DEV__
  ? 'http://localhost:3000'
  : 'https://api.production.com';

Common Workflows

Adding a New Screen

1

Create file in app/ directory

touch app/profile.tsx
2

Implement screen

app/profile.tsx
import { View, Text } from 'react-native';

export default function ProfileScreen() {
  return (
    <View>
      <Text>Profile Screen</Text>
    </View>
  );
}
3

Navigate to screen

import { Link } from 'expo-router';

<Link href="/profile">Go to Profile</Link>
4

Test on device

Fast Refresh automatically shows the new screen

Installing a New Package

1

Install with expo install

npx expo install expo-image-picker
2

Import in your code

import * as ImagePicker from 'expo-image-picker';
3

Use the API

const result = await ImagePicker.launchImageLibraryAsync();
4

Rebuild if needed

If the package has native code not in Expo Go:
npx expo prebuild
npx expo run:ios

Testing on Physical Device

1

Connect to same network

Ensure your device and computer are on the same WiFi
2

Start dev server

npx expo start
3

Scan QR code

  • iOS: Use Camera app
  • Android: Use Expo Go app
4

App loads

Your app opens and connects to the dev server
If scanning doesn’t work:
  1. Try tunnel mode:
    npx expo start --tunnel
    
  2. Check firewall settings
  3. Use USB connection:
    npx expo start --localhost
    
    Then connect via USB debugging

Performance Best Practices

Use React.memo and useMemo to prevent unnecessary re-renders:
import { memo, useMemo } from 'react';

const ExpensiveComponent = memo(({ data }) => {
  const processed = useMemo(() => processData(data), [data]);
  return <View>{/* render */}</View>;
});
Use dynamic imports for screens:
import { lazy, Suspense } from 'react';

const ProfileScreen = lazy(() => import('./ProfileScreen'));

<Suspense fallback={<Loading />}>
  <ProfileScreen />
</Suspense>
Use optimized image formats and sizes:
import { Image } from 'expo-image';

<Image
  source={{ uri: url }}
  contentFit="cover"
  placeholder={blurhash}
  transition={200}
/>
Use React DevTools Profiler:
import { Profiler } from 'react';

<Profiler id="Screen" onRender={onRenderCallback}>
  <Screen />
</Profiler>

Troubleshooting

Clear cache and restart:
npx expo start --clear
  1. Check for syntax errors
  2. Ensure file is in app/ or src/
  3. Manually reload: Press r in terminal
Check for:
  • JavaScript errors in terminal
  • Missing imports
  • Incorrect file paths
  • Network connectivity issues
Rebuild the app:
npx expo prebuild --clean
npx expo run:ios

Next Steps

Expo Go

Learn about the Expo Go app

Tutorial

Build a complete app

Build & Deploy

Ship your app to production

SDK Reference

Explore all SDK modules

Build docs developers (and LLMs) love