Expo Development Workflow and Best Practices
Master Expo development workflow with EAS Build, over-the-air updates, native modules, and production deployment strategies.

Expo Development Workflow and Best Practices
Expo simplifies React Native development by providing a comprehensive toolchain for building, testing, and deploying mobile apps. This article explores advanced Expo workflows, EAS (Expo Application Services), and best practices for production apps.
Understanding Expo Architecture
Expo provides:
- Expo SDK: Pre-built native modules
- EAS Build: Cloud-based native builds
- EAS Update: Over-the-air updates
- Expo Go: Development client
- Config Plugins: Custom native configuration
Project Setup
1. Initialize Expo Project
npx create-expo-app MyApp --template
cd MyApp2. Install EAS CLI
npm install -g eas-cli
eas login
eas build:configure3. Configure app.json
{
"expo": {
"name": "MyApp",
"slug": "my-app",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.yourcompany.myapp"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.yourcompany.myapp"
},
"plugins": [
"expo-router"
],
"extra": {
"eas": {
"projectId": "your-project-id"
}
}
}
}Development Workflow
1. Local Development
# Start development server
npx expo start
# Start with tunnel (for testing on physical devices)
npx expo start --tunnel
# Start with specific platform
npx expo start --ios
npx expo start --android2. Using Expo Go
Expo Go allows instant testing without building:
// App.tsx
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
export default function App() {
return (
<View style={styles.container}>
<Text>Open up App.tsx to start working on your app!</Text>
<StatusBar style="auto" />
</View>
);
}3. Development Build
For apps using custom native code:
# Create development build
eas build --profile development --platform ios
eas build --profile development --platform android
# Install on device
eas build:run -p iosEAS Build Configuration
eas.json Setup
{
"cli": {
"version": ">= 5.0.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"simulator": true
}
},
"preview": {
"distribution": "internal",
"ios": {
"simulator": false
}
},
"production": {
"autoIncrement": true,
"env": {
"API_URL": "https://api.production.com"
}
}
},
"submit": {
"production": {
"ios": {
"appleId": "your@email.com",
"ascAppId": "your-app-id"
},
"android": {
"serviceAccountKeyPath": "./service-account.json",
"track": "internal"
}
}
}
}Build Profiles
# Development build
eas build --profile development --platform all
# Preview build (for TestFlight/Internal Testing)
eas build --profile preview --platform ios
# Production build
eas build --profile production --platform allOver-the-Air Updates (EAS Update)
1. Setup EAS Update
eas update:configure2. Create Update
// app.json
{
"expo": {
"updates": {
"url": "https://u.expo.dev/your-project-id",
"enabled": true,
"checkAutomatically": "ON_LOAD",
"fallbackToCacheTimeout": 0
},
"runtimeVersion": "1.0.0"
}
}3. Publish Updates
# Publish update
eas update --branch production --message "Bug fixes"
# Publish with specific runtime version
eas update --branch production --runtime-version 1.0.04. Update Strategy
import * as Updates from 'expo-updates';
async function checkForUpdates() {
try {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
await Updates.fetchUpdateAsync();
await Updates.reloadAsync();
}
} catch (error) {
console.error('Error checking for updates:', error);
}
}
// Check on app start
useEffect(() => {
checkForUpdates();
}, []);Custom Native Modules
1. Config Plugins
// plugins/withCustomConfig.ts
import { ConfigPlugin } from '@expo/config-plugins';
const withCustomConfig: ConfigPlugin = (config) => {
// Modify native configuration
return config;
};
export default withCustomConfig;2. Using Config Plugins
{
"expo": {
"plugins": [
"./plugins/withCustomConfig",
[
"expo-camera",
{
"cameraPermission": "Allow $(PRODUCT_NAME) to access your camera"
}
]
]
}
}Environment Variables
1. Setup
# Install dotenv
npm install dotenv
# Create .env files
.env.development
.env.production2. Configuration
// app.config.js
require('dotenv').config({ path: `.env.${process.env.NODE_ENV}` });
export default {
expo: {
extra: {
apiUrl: process.env.API_URL,
apiKey: process.env.API_KEY,
},
},
};3. Usage
import Constants from 'expo-constants';
const apiUrl = Constants.expoConfig?.extra?.apiUrl;Testing
1. Unit Tests
// __tests__/App.test.tsx
import { render, screen } from '@testing-library/react-native';
import App from '../App';
test('renders correctly', () => {
render(<App />);
expect(screen.getByText('Welcome')).toBeTruthy();
});2. E2E Tests with Detox
// e2e/App.e2e.ts
describe('App', () => {
beforeAll(async () => {
await device.launchApp();
});
it('should show welcome screen', async () => {
await expect(element(by.id('welcome'))).toBeVisible();
});
});Performance Optimization
1. Code Splitting
import { lazy, Suspense } from 'react';
const HeavyScreen = lazy(() => import('./screens/HeavyScreen'));
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyScreen />
</Suspense>
);
}2. Asset Optimization
# Optimize images
npx expo-optimize
# Generate adaptive icons
npx expo prebuildDeployment
1. iOS Deployment
# Build for App Store
eas build --platform ios --profile production
# Submit to App Store
eas submit --platform ios --profile production2. Android Deployment
# Build for Play Store
eas build --platform android --profile production
# Submit to Play Store
eas submit --platform android --profile production3. Internal Distribution
# Build for internal testing
eas build --profile preview --platform all
# Share build URL
eas build:listCI/CD Integration
GitHub Actions Example
# .github/workflows/build.yml
name: Build and Submit
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm install
- run: npm install -g eas-cli
- run: eas build --platform all --profile production --non-interactive
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}Best Practices
1. Version Management
{
"expo": {
"version": "1.0.0",
"ios": {
"buildNumber": "1"
},
"android": {
"versionCode": 1
},
"runtimeVersion": "1.0.0"
}
}2. Error Tracking
import * as Sentry from '@sentry/react-native';
Sentry.init({
dsn: 'your-sentry-dsn',
enableInExpoDevelopment: true,
debug: __DEV__,
});3. Analytics
import * as Analytics from 'expo-firebase-analytics';
Analytics.logEvent('screen_view', {
screen_name: 'Home',
screen_class: 'HomeScreen',
});Troubleshooting
Common Issues
- Build Failures: Check EAS build logs
- Update Not Working: Verify runtime version matches
- Native Module Issues: Ensure config plugins are correct
- Performance: Use Expo Dev Tools for profiling
Conclusion
Expo provides a powerful toolchain for React Native development. By mastering EAS Build, EAS Update, config plugins, and deployment workflows, you can efficiently build, test, and deploy production-ready mobile applications. Follow best practices for versioning, error tracking, and performance optimization to create robust apps.
References
Want more insights?
Subscribe to our newsletter or follow us for more updates on software development and team scaling.
Contact Us