expo and react-native-cli represent two distinct approaches to building React Native applications. expo provides a managed workflow with a comprehensive suite of tools, services, and pre-configured native modules, allowing developers to build apps without touching native code directly. It simplifies setup, testing, and deployment through a unified ecosystem. In contrast, react-native-cli (often referred to as the React Native CLI) offers a bare-metal approach where developers initialize a project with direct access to native iOS and Android folders. This grants full control over native dependencies and custom native code but requires deeper knowledge of Xcode, Android Studio, and native build systems. While expo has evolved to support custom native code via "Prebuild," the fundamental difference lies in the default developer experience and the level of abstraction provided out of the box.
Choosing between expo and react-native-cli is one of the most critical architectural decisions in a React Native project. This choice dictates your build pipeline, how you handle native dependencies, and your team's daily workflow. While both ultimately render native UI components, they differ significantly in how they bridge the gap between JavaScript and the underlying operating systems.
expo creates a "managed" project. You do not see ios or android folders by default. The native code is hidden inside the Expo Go client app during development. You configure your app via a single app.json (or app.config.js) file.
# Expo: Creates a clean JS-only structure
npx create-expo-app@latest MyManagedApp
cd MyManagedApp
# No /ios or /android folders visible yet
react-native-cli creates a "bare" project. It immediately generates native folders (ios/, android/) containing Xcode and Gradle projects. You are responsible for managing these folders from the start.
# React Native CLI: Creates full native structure
npx @react-native-community/cli init MyBareApp
cd MyBareApp
# /ios and /android folders exist immediately with native code
This is the most significant technical divergence. Native modules (like Bluetooth, advanced camera features, or specific payment SDKs) require linking native code.
expo relies on its SDK for common modules. If a library supports "Expo Config Plugins," you can install it via JavaScript, and Expo automatically generates the necessary native code during a "Prebuild" step. If a library lacks a config plugin, you must run npx expo prebuild to generate native folders and manually link the code.
// Expo: Installing a module with Config Plugin support
// e.g., expo-camera
npx expo install expo-camera
// app.json configuration handles permissions automatically
{
"expo": {
"plugins": ["expo-camera"]
}
}
react-native-cli requires you to manually install CocoaPods (iOS) and update Gradle files (Android) for almost every native dependency. You directly edit native files to link libraries.
# React Native CLI: Manual linking steps often required
npm install react-native-bluetooth-manager
cd ios && pod install && cd ..
// You often must manually edit ios/Podfile or android/build.gradle
// to ensure native libraries are found by the compiler
expo shines with its development client. You can scan a QR code and run your JavaScript bundle instantly on the "Expo Go" app without compiling native code. For custom native code, you build a custom "Development Client" which acts like a personalized Expo Go.
# Expo: Run on device via QR code (Managed)
npx expo start
# Scan QR code with Expo Go app -> App loads instantly
# Expo: Run with custom native code (Prebuild)
npx expo run:ios
# Builds a custom dev client once, then supports fast refresh
react-native-cli requires you to compile the native app every time you change native code. You must have Xcode (for iOS) or Android Studio (for Android) installed and configured. The initial build time is significantly longer.
# React Native CLI: Must compile native code
npx react-native run-ios
# Launches Simulator, compiles Swift/Obj-C, links binaries, then loads JS
npx react-native run-android
# Launches Emulator, runs Gradle build, installs APK, then loads JS
expo provides Over-The-Air (OTA) updates via EAS Update. You can push JavaScript changes to users instantly without going through the App Store review process (within limits). Binary distribution is handled by EAS Build, which compiles your app in the cloud.
// Expo: Pushing an OTA update
npx eas update --branch production --message "Fix login bug"
// Users receive the fix next time they open the app, no store update needed
react-native-cli typically relies on third-party services like CodePush for OTA updates, which require manual integration. Binary builds are usually managed locally or via custom CI/CD pipelines (GitHub Actions, Bitrise), requiring you to maintain build certificates and provisioning profiles manually.
# React Native CLI: Traditional Store Release
# 1. Archive build in Xcode / Build signed APK in Android Studio
# 2. Upload binary to App Store Connect / Google Play Console
# 3. Wait for review process
# No built-in OTA mechanism without adding libraries like react-native-code-push
A common misconception is that choosing expo locks you in forever. Modern Expo uses "Continuous Native Generation." You keep your native folders in .gitignore and regenerate them whenever config changes.
# Expo: Regenerating native code based on config
npx expo prebuild --clean
# Deletes ios/android folders and recreates them based on app.json and installed plugins
In react-native-cli, the native folders are the source of truth. You edit them directly. If you break the build configuration, you must debug Gradle or Xcode project files manually.
# React Native CLI: Native folders are permanent
# You manually edit ios/MyApp.xcworkspace or android/app/build.gradle
# No automated regeneration tool resets these files to a default state easily
Despite the workflow differences, both approaches share the same rendering engine and core capabilities.
Both render actual native views (UIView on iOS, View on Android). The performance characteristics of the UI thread are identical.
// Works identically in both Expo and CLI
import { View, Text, StyleSheet } from 'react-native';
export default function App() {
return (
<View style={styles.container}>
<Text>Hello World</Text>
</View>
);
}
Both support Fast Refresh, preserving local state while editing code.
// Both environments support this workflow
// Edit component -> Save -> UI updates instantly without losing state
const [count, setCount] = useState(0);
Business logic, state management (Redux, Zustand), and routing (React Navigation) work exactly the same way.
// React Navigation works in both
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
// Stack navigator configuration is identical
const Stack = createNativeStackNavigator();
| Feature | expo | react-native-cli |
|---|---|---|
| Native Folders | Hidden by default (generated via Prebuild) | Visible and mandatory from start |
| Setup Time | Minutes (QR code ready) | Hours (Xcode/Android Studio config) |
| Native Modules | Auto-linked via Config Plugins | Manual linking (CocoaPods/Gradle) |
| Build System | Cloud-based (EAS Build) or Local Prebuild | Local (Xcode/Gradle) or Custom CI |
| OTA Updates | Built-in (EAS Update) | Requires 3rd party (e.g., CodePush) |
| App Size | Slightly larger (includes Expo modules) | Minimal (only what you link) |
| Flexibility | High (via Prebuild), but opinionated | Unlimited (full native access) |
expo is the modern standard for most React Native development. It removes the friction of native tooling, allowing teams to ship faster. With the introduction of Prebuild and Config Plugins, the gap between "managed" and "bare" has nearly closed. You only lose out if you need to modify native code manually on a daily basis without using the plugin system.
react-native-cli remains relevant for highly specialized cases: apps that rely heavily on legacy native code, require specific non-standard build configurations, or teams that strictly forbid cloud-based build services. However, for greenfield projects, the maintenance burden of managing native folders manually often outweighs the benefits unless you have dedicated native engineers on staff.
Final Thought: Start with expo. If you hit a wall where a Config Plugin doesn't exist for a critical native library, you can run npx expo prebuild and effectively transition to a CLI-like workflow while keeping the Expo tooling benefits. Starting with react-native-cli gives you no such easy path to simplify later.
Choose expo if you want to accelerate development with a managed workflow that handles native configuration, provides over-the-air updates, and offers a rich set of pre-built modules. It is ideal for startups, MVPs, and teams that prefer to focus on JavaScript logic rather than maintaining native build configurations. With Expo's 'Prebuild' feature, you can still eject to custom native code later if specific library requirements arise, making it a flexible starting point for most projects.
Choose react-native-cli if your project requires immediate, deep customization of native code (iOS/Android) from day one, or if you must integrate legacy native libraries that are not compatible with Expo's managed environment. This approach is suitable for large enterprises with dedicated mobile engineering teams who need full control over the native build pipeline, third-party native dependencies, and specific platform-level optimizations without the abstraction layer of the Expo SDK.