Expo: React Native Development Framework
Complete setup and integration guide for Expo - the official React Native framework recommended by the React Native team. Covers installation, managed vs bare workflows, native modules, EAS Build, over-the-air updates, the New Architecture, and production deployment patterns for building iOS and Android apps with TypeScript.
- Step 1
What is Expo?
Expo is an open-source framework built on top of React Native that simplifies the process of building, testing, and deploying mobile apps. With over 46,000 stars on GitHub, Expo is the official framework recommended by the React Native team for all new projects starting in 2026. Expo handles builds, deployments, and native APIs so you can ship iOS and Android apps from TypeScript without touching Xcode or Android Studio. The framework provides 50+ pre-built native modules including camera, location, file system, biometrics, push notifications, and more. As of 2026, with Expo SDK 55 and React Native 0.83, the New Architecture (featuring JSI, Fabric renderer, and TurboModules) is now mandatory, delivering near-native performance with improved startup times and 60fps animations.
- Step 2
Core Architecture and Concepts
Expo's 2026 architecture is built on the React Native New Architecture, which fundamentally changes how JavaScript communicates with native code:
JSI (JavaScript Interface) - Makes native calls synchronous with direct access to native objects from JavaScript. No more asynchronous bridge overhead for layout measurements, native API calls, or file system access.
Fabric Renderer - Integrates with React 18's concurrent model for smoother UI updates and better performance.
TurboModules - Enables lazy-loading of native modules, reducing startup time and memory usage.
Hermes Engine - JavaScript engine optimized for React Native. React Native 0.84 introduces Hermes V1 as the default, cutting startup time in half compared to JavaScriptCore.
Metro Bundler - The official bundler maintained by Meta that serves as the central build tool, with first-class support for debugging and hot module replacement.
Expo Router - File-based routing for mobile apps using the same mental model as Next.js. Routes are files, layouts are wrappers, and navigation is automatic.
EAS (Expo Application Services) - Cloud infrastructure for builds, submissions, and over-the-air updates without requiring local Xcode or Android Studio installations.
Expo Architecture (2026): ├── React Native New Architecture │ ├── JSI (synchronous native calls) │ ├── Fabric (concurrent renderer) │ └── TurboModules (lazy loading) ├── Hermes V1 (optimized JS engine) ├── Metro (bundler + HMR) ├── Expo Router (file-based routing) ├── Expo SDK 50+ modules └── EAS (builds, updates, submissions) - Step 3
System Requirements
Before installing Expo, ensure your development environment meets these requirements. Expo supports macOS, Windows, and Linux for development, though macOS is required for building iOS apps locally with Xcode (note that EAS Build can build iOS apps in the cloud without a Mac).
# Required Node.js 18+ (recommended: 22.14.0 or later) npm or yarn package manager # Check your Node.js version node --version # Should show v18.0.0 or higher # Optional (for local native builds only) Xcode 14+ (macOS only, for iOS development) Android Studio (for Android development) # Strongly recommended Expo Go app (iOS/Android) for testing on physical devices VS Code with React Native Tools extension - Step 4
Installation and Project Setup
Expo CLI is now included with the expo package - no global installation needed. The create-expo-app command scaffolds a new project with sensible defaults and the latest SDK version. The process is streamlined and works on all platforms.
# Create a new Expo project npx create-expo-app my-app # Navigate into the project cd my-app # Start the development server npx expo start # This will show a QR code and menu with options: # › Press a │ open Android # › Press i │ open iOS simulator # › Press w │ open web # › Press r │ reload app # › Press j │ open debugger # › Press m │ toggle menu⚠ Heads up: The Expo Go app (available on iOS App Store and Google Play Store) is great for quick testing, but has limitations. For production features like custom native code or certain libraries, you'll need to create a development build using EAS Build. - Step 5
Testing on Physical Devices
Expo Go is the fastest way to preview your app on a real device during development. Simply scan the QR code displayed when you run npx expo start. This works over your local network without cables or complex setup.
# 1. Install Expo Go from app stores: # iOS: https://apps.apple.com/app/expo-go/id982107779 # Android: https://play.google.com/store/apps/details?id=host.exp.exponent # 2. Start your dev server npx expo start # 3. Scan the QR code: # - iOS: Use the Camera app (built-in QR scanner) # - Android: Use the Expo Go app's built-in scanner # Your app will load on the device and hot-reload as you edit - Step 6
Project Structure Overview
Expo projects using Expo Router follow a file-based routing convention. The app directory contains your routes, and the structure mirrors your app's navigation hierarchy.
my-app/ ├── app/ # Expo Router routes (file-based) │ ├── _layout.tsx # Root layout wrapper │ ├── index.tsx # Home screen (/) │ ├── about.tsx # About screen (/about) │ └── (tabs)/ # Tab navigation group │ ├── _layout.tsx │ ├── home.tsx │ └── profile.tsx ├── components/ # Reusable React components ├── constants/ # Colors, config, etc. ├── hooks/ # Custom React hooks ├── assets/ # Images, fonts, etc. ├── app.json # Expo configuration ├── package.json └── tsconfig.json - Step 7
Understanding Managed vs Bare Workflow
Expo offers two workflow approaches, each with different tradeoffs between simplicity and control:
Managed Workflow (Recommended for Most Apps) All native code is hidden and managed by Expo. You write only TypeScript/JavaScript, and building is handled through EAS Build. You get 50+ native APIs out of the box, over-the-air updates without app store approval, and no need for Xcode or Android Studio. This is the default when you run create-expo-app.
Bare Workflow (When You Need Custom Native Code) Gives you complete control over the native projects. You can still use most Expo SDK modules and EAS Build, but you'll need to work directly with Xcode and Android Studio. Choose this when you need complex native integrations, custom SDKs, or platform-specific optimizations that aren't available through Expo modules.
In 2026, the gap has narrowed significantly - most apps can stay in the managed workflow thanks to config plugins and the Expo Modules API. If a native feature isn't available, you can often write a custom module or use a config plugin without ejecting to bare workflow.
# Start with managed workflow (default) npx create-expo-app my-app # If you later need native code access, you can migrate: # This generates ios/ and android/ directories npx expo prebuild # After prebuild, you can: # - Edit native code directly # - Add custom native modules # - Still use Expo SDK and EAS Build⚠ Heads up: Once you run expo prebuild and commit the native directories, you're responsible for managing them. Config plugins let you customize native code without manually editing it, which is often a better approach. - Step 8
First Expo App with Expo Router
Expo Router brings Next.js-style file-based routing to React Native. If you've built a Next.js app, the mental model is identical. Here's a simple app with two screens and navigation.
// app/_layout.tsx - Root layout import { Stack } from 'expo-router'; export default function RootLayout() { return <Stack />; } // app/index.tsx - Home screen import { View, Text, Button } from 'react-native'; import { Link } from 'expo-router'; export default function Home() { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text style={{ fontSize: 24, marginBottom: 20 }}>Welcome to Expo!</Text> <Link href="/about" asChild> <Button title="Go to About" /> </Link> </View> ); } // app/about.tsx - About screen import { View, Text } from 'react-native'; import { useRouter } from 'expo-router'; export default function About() { const router = useRouter(); return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text style={{ fontSize: 24 }}>About Screen</Text> <Button title="Go Back" onPress={() => router.back()} /> </View> ); } - Step 9
Key Expo SDK Modules
Expo SDK provides over 50 pre-built native modules that work out of the box in the managed workflow. Here are the most commonly used ones. These modules are maintained by the Expo team and tested across platforms.
// Camera - Take photos and videos import { CameraView, useCameraPermissions } from 'expo-camera'; // Location - Get device location import * as Location from 'expo-location'; const location = await Location.getCurrentPositionAsync({}); // File System - Read/write files import * as FileSystem from 'expo-file-system'; const content = await FileSystem.readAsStringAsync(fileUri); // Notifications - Local and push notifications import * as Notifications from 'expo-notifications'; const token = await Notifications.getExpoPushTokenAsync(); // Image Picker - Select from gallery import * as ImagePicker from 'expo-image-picker'; const result = await ImagePicker.launchImageLibraryAsync(); // SQLite - Local database import * as SQLite from 'expo-sqlite'; const db = SQLite.openDatabaseSync('mydb.db'); // Secure Store - Encrypted key-value storage import * as SecureStore from 'expo-secure-store'; await SecureStore.setItemAsync('key', 'value'); // Haptics - Vibration feedback import * as Haptics from 'expo-haptics'; await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); // Biometric Authentication import * as LocalAuthentication from 'expo-local-authentication'; const result = await LocalAuthentication.authenticateAsync(); - Step 10
Setting Up Push Notifications (2026 Update)
Expo provides a unified API for push notifications across iOS and Android. As of SDK 53, push notifications no longer work in Expo Go on Android - you must use a development build. Expo can handle the push notification service, or you can send directly from FCM/APNs.
// app/_layout.tsx or app/index.tsx import * as Notifications from 'expo-notifications'; import { useEffect, useRef, useState } from 'react'; import { Platform } from 'react-native'; // Configure how notifications are displayed Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowAlert: true, shouldPlaySound: true, shouldSetBadge: false, }), }); export default function App() { const [expoPushToken, setExpoPushToken] = useState(''); const notificationListener = useRef<Notifications.Subscription>(); useEffect(() => { // Get push token registerForPushNotificationsAsync().then(token => { setExpoPushToken(token ?? ''); }); // Listen for notifications notificationListener.current = Notifications.addNotificationReceivedListener( notification => { console.log('Notification received:', notification); } ); return () => { notificationListener.current && Notifications.removeNotificationSubscription(notificationListener.current); }; }, []); return ( // Your app here ); } async function registerForPushNotificationsAsync() { if (Platform.OS === 'android') { await Notifications.setNotificationChannelAsync('default', { name: 'default', importance: Notifications.AndroidImportance.MAX, }); } const { status: existingStatus } = await Notifications.getPermissionsAsync(); let finalStatus = existingStatus; if (existingStatus !== 'granted') { const { status } = await Notifications.requestPermissionsAsync(); finalStatus = status; } if (finalStatus !== 'granted') { return null; } const token = await Notifications.getExpoPushTokenAsync({ projectId: 'your-project-id', // From app.json }); return token.data; }⚠ Heads up: Push notifications require a development build (not Expo Go) on Android as of SDK 53. Run npx eas build --profile development --platform android to create one, or use the iOS simulator which still supports notifications in Expo Go. - Step 11
Working with Native Device APIs
Many device APIs require user permissions. Expo provides a consistent pattern for requesting permissions across all modules. Always request permissions before using sensitive APIs.
import { useState } from 'react'; import { Button, View, Image, Text } from 'react-native'; import * as ImagePicker from 'expo-image-picker'; import * as Location from 'expo-location'; export default function PermissionsExample() { const [image, setImage] = useState<string | null>(null); const [location, setLocation] = useState<string>(''); const pickImage = async () => { // Request permission const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (status !== 'granted') { alert('Permission to access gallery is required!'); return; } // Launch picker const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, aspect: [4, 3], quality: 1, }); if (!result.canceled) { setImage(result.assets[0].uri); } }; const getLocation = async () => { const { status } = await Location.requestForegroundPermissionsAsync(); if (status !== 'granted') { alert('Permission to access location is required!'); return; } const loc = await Location.getCurrentPositionAsync({}); setLocation(`${loc.coords.latitude}, ${loc.coords.longitude}`); }; return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Button title="Pick an image" onPress={pickImage} /> {image && <Image source={{ uri: image }} style={{ width: 200, height: 200 }} />} <Button title="Get Location" onPress={getLocation} /> {location && <Text>Location: {location}</Text>} </View> ); } - Step 12
Configuration with app.json
The app.json (or app.config.js for dynamic config) file controls your app's metadata, build settings, permissions, and more. This is where you configure everything from the app name to which device permissions you need.
{ "expo": { "name": "My Awesome App", "slug": "my-awesome-app", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", "splash": { "image": "./assets/splash.png", "resizeMode": "contain", "backgroundColor": "#ffffff" }, "ios": { "supportsTablet": true, "bundleIdentifier": "com.yourcompany.myapp", "infoPlist": { "NSCameraUsageDescription": "This app uses the camera to take photos.", "NSLocationWhenInUseUsageDescription": "This app uses location to show nearby places." } }, "android": { "package": "com.yourcompany.myapp", "permissions": [ "CAMERA", "ACCESS_FINE_LOCATION", "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE" ], "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" } }, "plugins": [ "expo-router", [ "expo-camera", { "cameraPermission": "Allow $(PRODUCT_NAME) to access your camera" } ] ], "extra": { "eas": { "projectId": "your-project-id" } } } }⚠ Heads up: iOS permission descriptions (like NSCameraUsageDescription) are required for app store approval. Android permissions are automatically added by Expo modules, but you can declare them explicitly for clarity. - Step 13
Debugging with React Native DevTools
Expo integrates with React Native DevTools (formerly Chrome DevTools) for debugging. Since Hermes is the default engine, debugging happens directly on the device rather than in a browser tab, providing more accurate performance profiling.
# Start your app npx expo start # Press 'j' to open the debugger # This launches React Native DevTools in Chrome or Edge # Available debugging features: # - Console logs (console.log, warn, error) # - Network requests inspection # - React component tree # - Performance profiling # - Source maps for TypeScript # Alternative: VS Code debugging # 1. Install "React Native Tools" extension # 2. Add launch configuration: # { # "type": "react-native", # "request": "launch", # "name": "Debug Expo", # "cwd": "${workspaceFolder}" # } # 3. Press F5 to start debugging with breakpoints - Step 14
EAS Build: Cloud Builds Without Xcode
EAS Build is Expo's cloud build service that compiles your app for iOS and Android without requiring local Xcode or Android Studio installations. This is especially valuable for Windows/Linux developers building iOS apps, or for CI/CD pipelines. The free plan includes 30 builds per month.
# Install EAS CLI globally (one-time setup) npm install -g eas-cli # Login to your Expo account eas login # Configure EAS for your project eas build:configure # This creates eas.json with build profiles # Build for development (includes dev tools, runs on any device) eas build --profile development --platform ios eas build --profile development --platform android # Build for preview (production-like, for testing) eas build --profile preview --platform all # Build for production (app store submission) eas build --profile production --platform all # Check build status eas build:list # Download a completed build eas build:download --platform ios --latest - Step 15
EAS Build Configuration
The eas.json file defines build profiles for different scenarios: development builds include debugging tools, preview builds are for testing the production experience, and production builds are for app store submission.
{ "build": { "development": { "developmentClient": true, "distribution": "internal", "ios": { "simulator": true } }, "preview": { "distribution": "internal", "env": { "API_URL": "https://staging-api.example.com" } }, "production": { "env": { "API_URL": "https://api.example.com" }, "autoIncrement": true } }, "submit": { "production": { "ios": { "appleId": "your-apple-id@example.com", "ascAppId": "1234567890" }, "android": { "serviceAccountKeyPath": "./service-account-key.json", "track": "production" } } } } - Step 16
Over-the-Air (OTA) Updates with EAS Update
One of Expo's most powerful features is the ability to push JavaScript and asset updates directly to users without going through app store review. This is perfect for bug fixes, content updates, and A/B testing. Note that native code changes still require a full app store submission.
# Install the EAS Update library npx expo install expo-updates # Configure EAS Update in app.json # Add to your expo config: # "updates": { # "url": "https://u.expo.dev/[your-project-id]" # } # Publish an update eas update --branch production --message "Fix login bug" # Create preview channels for testing eas update --branch staging --message "Test new feature" # View update history eas update:list # Roll back to a previous update eas update:republish --group <update-group-id> # In your app, you can check for updates programmatically: # import * as Updates from 'expo-updates'; # const update = await Updates.checkForUpdateAsync(); # if (update.isAvailable) { # await Updates.fetchUpdateAsync(); # await Updates.reloadAsync(); # }⚠ Heads up: OTA updates can only change JavaScript code and assets. Native code changes (new libraries with native modules, permission changes, app.json native config) require a new build and app store submission. - Step 17
Environment Variables and Secrets
Expo supports environment variables for configuration that changes between development, staging, and production. Never commit secrets to version control - use EAS Secrets for sensitive values.
# Create .env file (add to .gitignore) API_URL=https://api.example.com API_KEY=your-dev-api-key # Install environment variable support npx expo install expo-constants # Access in your app with process.env (requires extra config) # Or use app.config.js for dynamic configuration: # export default { # expo: { # extra: { # apiUrl: process.env.API_URL, # }, # }, # }; # Access via Constants: # import Constants from 'expo-constants'; # const apiUrl = Constants.expoConfig?.extra?.apiUrl; # For EAS Build, add secrets (not committed to repo): eas secret:create --scope project --name API_KEY --value your-prod-api-key # List secrets eas secret:list # Reference in eas.json: # "production": { # "env": { # "API_KEY": "$API_KEY" # } # } - Step 18
App Store Submission with EAS Submit
EAS Submit automates the process of uploading your app to the Apple App Store and Google Play Store. This eliminates the need for manual uploads through Xcode or the Play Console.
# iOS Submission Requirements: # 1. Apple Developer account ($99/year) # 2. App created in App Store Connect # 3. Production build completed # Submit to App Store eas submit --platform ios --latest # Or specify a build ID: eas submit --platform ios --id <build-id> # Android Submission Requirements: # 1. Google Play Developer account ($25 one-time) # 2. App created in Play Console # 3. Service account key JSON (for API access) # 4. Production build completed # Submit to Google Play eas submit --platform android --latest # Submit to internal testing track first eas submit --platform android --track internal # Configure submission in eas.json (see EAS Build Configuration step) # Then just run: eas submit --platform all⚠ Heads up: First-time App Store submissions must include privacy policy, app icons, screenshots, and descriptions. Allow 24-48 hours for Apple review. Google Play review is typically faster (hours to 1 day). - Step 19
Adding Third-Party Libraries
Most React Native libraries work with Expo. For libraries with native code, check if they include a config plugin (the modern way) or if you need to run expo prebuild (which moves you toward bare workflow). The Expo documentation has a compatibility checker.
# Install a library with native code npx expo install react-native-maps # If it has a config plugin, add to app.json: # "plugins": [ # "react-native-maps" # ] # Expo automatically applies the plugin during builds # No need to edit native code manually! # For libraries without config plugins: # 1. Check if someone created a community plugin # 2. Write a custom config plugin # 3. Or use expo prebuild for manual native access # Check library compatibility: # Visit: https://reactnative.directory/ # Filter by "Expo Go" to see what works in managed workflow # Some popular libraries with config plugins: # - @react-native-firebase/app (via @react-native-firebase/expo-config) # - react-native-reanimated (via Expo SDK) # - react-native-gesture-handler (via Expo SDK) # - react-native-maps (config plugin included) # - expo-camera, expo-notifications, etc. (all Expo modules) - Step 20
Performance Optimization
React Native and Expo provide several tools and techniques for optimizing app performance. The New Architecture (now mandatory in 2026) already provides significant improvements, but you should still follow best practices.
// 1. Use React.memo for expensive components import { memo } from 'react'; const ExpensiveComponent = memo(({ data }) => { return <View>{/* Complex rendering */}</View>; }); // 2. Use useMemo and useCallback appropriately import { useMemo, useCallback } from 'react'; function MyComponent({ items }) { const processedItems = useMemo( () => items.map(item => ({ ...item, processed: true })), [items] ); const handlePress = useCallback(() => { console.log('Pressed'); }, []); return <View>{/* Use processedItems */}</View>; } // 3. Optimize FlatList rendering import { FlatList } from 'react-native'; <FlatList data={items} renderItem={({ item }) => <ItemComponent item={item} />} keyExtractor={(item) => item.id} // Performance props: removeClippedSubviews={true} maxToRenderPerBatch={10} updateCellsBatchingPeriod={50} initialNumToRender={10} windowSize={21} getItemLayout={(data, index) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index, })} /> // 4. Use Hermes (enabled by default) // Hermes is now the default engine - no action needed! // 5. Profile with React DevTools // Press 'j' in Expo dev server, use Profiler tab - Step 21
Testing Your Expo App
Expo works with popular testing frameworks. Jest is included by default for unit tests, and you can add Detox or Maestro for end-to-end testing.
# Jest is already configured in new Expo projects # Add test script to package.json if not present: # "scripts": { # "test": "jest" # } # Run tests npm test # Example test file: __tests__/App.test.tsx import renderer from 'react-test-renderer'; import App from '../App'; it('renders correctly', () => { const tree = renderer.create(<App />).toJSON(); expect(tree).toBeTruthy(); }); # For E2E testing, install Maestro (recommended) # https://maestro.mobile.dev/ curl -Ls "https://get.maestro.mobile.dev" | bash # Create a flow file: .maestro/app.yaml # appId: com.yourcompany.myapp # --- # - launchApp # - tapOn: "Login" # - inputText: "user@example.com" # - tapOn: "Submit" # - assertVisible: "Welcome" # Run E2E tests maestro test .maestro/app.yaml # For React Native Testing Library: npm install --save-dev @testing-library/react-native # Example test: import { render, fireEvent } from '@testing-library/react-native'; import { MyButton } from '../MyButton'; test('button press', () => { const onPress = jest.fn(); const { getByText } = render(<MyButton onPress={onPress} />); fireEvent.press(getByText('Press me')); expect(onPress).toHaveBeenCalled(); }); - Step 22
Production Deployment Checklist
Before submitting to app stores, ensure you've completed these essential steps. Missing any of these can result in rejection or poor user experience.
Pre-Launch Checklist: □ App Icons & Splash Screen - Icon: 1024x1024 PNG (iOS and Android) - Adaptive icon for Android - Splash screen image □ App Store Assets - Screenshots for all device sizes - App description and keywords - Privacy policy URL (required by Apple) - Support URL □ Configuration - Unique bundle identifier (iOS) / package name (Android) - Correct version number and build number - All required permissions with descriptions - Remove console.log statements - Disable dev warnings □ Testing - Test on physical iOS and Android devices - Test production build, not just development - Verify deep linking works - Test push notifications - Check offline behavior - Verify analytics are tracking □ Performance - App starts in < 3 seconds - No memory leaks - Images optimized - Minimal bundle size □ Compliance - Privacy policy covers data collection - GDPR compliance (if applicable) - COPPA compliance (if targeting children) - Terms of service □ Post-Launch - Set up EAS Update for quick fixes - Configure error tracking (Sentry, etc.) - Set up analytics (Expo Analytics, Firebase, etc.) - Monitor crash reports - Step 23
Common Debugging Tips and Solutions
Here are solutions to common issues you might encounter when developing with Expo:
Issue: "Metro bundler stuck or slow" Solution: npx expo start --clear Issue: "Can't connect to dev server on device" Solution: - Ensure phone and computer on same Wi-Fi - Try tunnel mode: npx expo start --tunnel - Check firewall isn't blocking Metro (port 8081) Issue: "Module not found error after installing library" Solution: 1. Stop dev server 2. npx expo install <package> 3. Clear cache: npx expo start --clear 4. For native modules, rebuild: eas build --profile development Issue: "Push notifications not working" Solution: - SDK 53+: Must use development build on Android (not Expo Go) - Ensure you have projectId in app.json - Check permissions are granted - Verify push token is being sent to your backend Issue: "App crashes on launch (production build)" Solution: - Check logs: eas build:list → view build logs - Verify app.json config is correct - Ensure all assets are referenced correctly - Check for missing permissions - Use EAS Update to push a fix Issue: "OTA update not appearing" Solution: - Updates only check on app launch (cold start) - Runtime channel must match update branch - Check: eas update:list - Force check: Updates.checkForUpdateAsync() Issue: "Build failed on EAS" Solution: - Read the full build logs - Common causes: missing credentials, invalid config, incompatible dependencies - Try local build first: npx expo run:ios or npx expo run:android Debug Commands: - npx expo-doctor (diagnose common issues) - npx expo config --type public (view resolved config) - npx react-native info (environment info) - Step 24
When to Use Expo vs Bare React Native
Choosing between Expo and bare React Native depends on your specific requirements. Here's a decision framework based on 2026 capabilities:
Choose Expo When:
- Building MVPs or prototypes rapidly
- Team has limited native iOS/Android experience
- Need fast iteration and OTA updates
- Standard functionality (camera, notifications, location, etc.)
- Small to medium-sized teams
- Want to avoid Xcode and Android Studio
- Need cloud builds (EAS Build)
Choose Bare React Native When:
- Require complex custom native code not available in Expo SDK
- Need specific native SDKs without config plugins
- Working with legacy native code that must integrate with React Native
- Require platform-specific optimizations beyond what Expo provides
- Have experienced native developers on team
The Gap Has Narrowed Significantly: In 2026, Expo supports production-scale applications with custom native modules via the Expo Modules API, config plugins for native configuration, and access to the full New Architecture. Approximately 83% of SDK 54 projects use Expo with the New Architecture. Many apps that previously required bare React Native can now stay in the Expo managed workflow.
Expo Advantages (2026): ✓ No Xcode/Android Studio required ✓ 50+ pre-built native modules ✓ EAS Build (cloud builds) ✓ OTA updates ✓ Faster development cycle ✓ File-based routing (Expo Router) ✓ Automatic app store submission ✓ Free tier available ✓ New Architecture fully supported Expo Limitations: ✗ App size ~2-4 MB larger (includes Expo SDK) ✗ Some obscure native libraries lack config plugins ✗ Must use development builds for some features (not Expo Go) ✗ OTA updates can't change native code Bare React Native Advantages: ✓ Full control over native code ✓ Can use any native library ✓ Slightly smaller app size ✓ Direct access to latest React Native features Bare React Native Tradeoffs: ✗ Requires Xcode and Android Studio ✗ Manual native configuration ✗ Longer build times ✗ Native expertise required ✗ No built-in OTA updates ✗ Manual app store submissions - Step 25
Advanced: Custom Native Modules with Expo Modules API
If you need a native feature that isn't in the Expo SDK and no library exists, you can write a custom native module using the Expo Modules API. This is easier than writing traditional React Native native modules and works in both managed and bare workflows.
# Create a local Expo module npx create-expo-module my-native-module # This creates a module structure: # my-native-module/ # ├── android/ # Android native code (Kotlin) # ├── ios/ # iOS native code (Swift) # ├── src/ # TypeScript API # └── expo-module.config.json # Example module: MyNativeModule/src/index.ts import { requireNativeModule } from 'expo-modules-core'; const MyNativeModule = requireNativeModule('MyNativeModule'); export function getDeviceName(): string { return MyNativeModule.getDeviceName(); } # iOS implementation (Swift): MyNativeModule/ios/MyNativeModule.swift import ExpoModulesCore public class MyNativeModule: Module { public func definition() -> ModuleDefinition { Name("MyNativeModule") Function("getDeviceName") { return UIDevice.current.name } } } # Android implementation (Kotlin): MyNativeModule/android/src/main/java/expo/modules/mynativemodule/MyNativeModule.kt package expo.modules.mynativemodule import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition class MyNativeModule : Module() { override fun definition() = ModuleDefinition { Name("MyNativeModule") Function("getDeviceName") { return android.os.Build.MODEL } } } # Install in your app: npx expo install ./my-native-module # Use in your app: import { getDeviceName } from 'my-native-module'; const deviceName = getDeviceName();⚠ Heads up: Custom native modules require running expo prebuild and creating development builds. They can't be used with Expo Go. Consider searching npm and GitHub first - someone may have already built what you need. - Step 26
Monitoring and Analytics
Production apps need monitoring for crashes, errors, and user analytics. Expo integrates with popular services, and some features are built into EAS.
# Option 1: Sentry (Error tracking) npm install @sentry/react-native npx expo install expo-dev-client # Configure in app.json: # "plugins": [ # [ # "@sentry/react-native/expo", # { # "organization": "your-org", # "project": "your-project" # } # ] # ] # Initialize in app/_layout.tsx: import * as Sentry from '@sentry/react-native'; Sentry.init({ dsn: 'your-sentry-dsn', enableInExpoDevelopment: false, debug: false, }); # Option 2: Firebase Analytics npx expo install expo-firebase-analytics # Follow Firebase setup guide # Option 3: Expo Analytics (built into EAS) # Automatically enabled for EAS projects # View in Expo dashboard: expo.dev/analytics # Track custom events: import * as Analytics from 'expo-firebase-analytics'; Analytics.logEvent('purchase', { item_id: 'SKU_123', price: 9.99, currency: 'USD', }); # Performance monitoring: import * as Sentry from '@sentry/react-native'; const transaction = Sentry.startTransaction({ name: 'load-user-profile', }); // ... do work ... transaction.finish(); - Step 27
Resources and Next Steps
Official Documentation:
- Expo Docs: https://docs.expo.dev/
- React Native Docs: https://reactnative.dev/
- Expo Router: https://docs.expo.dev/router/introduction/
GitHub Repositories:
- Expo: https://github.com/expo/expo (46K+ stars)
- React Native: https://github.com/facebook/react-native (120K+ stars)
Essential Tools:
- Expo Dashboard: https://expo.dev/ (manage projects, builds, updates)
- React Native Directory: https://reactnative.directory/ (library compatibility)
- EAS CLI: Global tool for builds and submissions
Community:
- Expo Discord: https://chat.expo.dev/
- Expo Forums: https://forums.expo.dev/
- React Native Community Discord: https://www.reactiflux.com/
- Stack Overflow: Use tags [expo] and [react-native]
Learning Resources:
- Expo YouTube Channel: Official tutorials and talks
- React Native Express: https://www.reactnative.express/ (free interactive guide)
- Expo Examples: https://github.com/expo/examples (sample apps)
- Official Blog: https://blog.expo.dev/
Getting Help:
- For bugs: File issues on GitHub (expo/expo)
- For questions: Expo Forums or Discord
- For paid support: Expo offers enterprise plans with SLA
Next Steps:
- Complete the Expo tutorial: https://docs.expo.dev/tutorial/introduction/
- Build a simple app using Expo Router and Expo SDK modules
- Set up EAS Build and create development builds
- Experiment with OTA updates
- Deploy a production app to both app stores
- Join the Expo community on Discord for ongoing support
Feature requests
Sign in to suggest features or vote on existing ones.
No feature requests yet.
Discussion
Sign in to join the discussion.
No comments yet.