Managed vs Bare workflow
| Managed workflow | Bare workflow | |
|---|---|---|
| Native code | Hidden โ Expo manages it | Fully accessible |
| Customization | Limited to Expo SDK | Unlimited |
| Adding native deps | Must use Expo config plugins | pod install / gradle sync |
| Expo Go support | Yes | No (use Expo Dev Client) |
| Build | EAS Build | EAS Build or local Xcode/Android Studio |
| When to use | New apps, standard features | Custom native code, existing RN projects |
Managed workflow is the fastest way to start โ Expo handles the native project, you write only JavaScript. The tradeoff: you can only use native capabilities that Expoโs SDK provides (or that have Expo config plugins).
Bare workflow is just a standard React Native project with Expo tooling layered on top. Full access to native code, any library, any config.
When to leave managed (eject)
You need to leave managed when:
- A native library requires custom native code with no config plugin
- You need to modify
AppDelegate.swift,MainActivity.kt, or deep build config - A required SDK or API isnโt in the Expo SDK and has no config plugin
Ejecting is now called โprebuildโ โ npx expo prebuild generates the native ios/ and android/ folders without changing your JS code. You keep Expo Router, Expo SDK, and EAS Build โ you just now have full native control.
Expo Go โ for development, not production
Expo Go is a pre-built RN runtime you install on your device. It lets you scan a QR code and instantly run your dev app โ no compilation needed.
Limitation: it can only run Expo-managed apps using the Expo SDK. If your app has custom native code, Expo Go canโt run it โ you need Expo Dev Client (a custom Expo Go you build yourself that includes your native dependencies).
EAS Build โ cloud builds
EAS (Expo Application Services) Build runs your builds on Expoโs cloud servers โ iOS and Android โ without needing a Mac for iOS builds.
npx expo install eas-cli
eas login
eas build:configure # generates eas.json
# Build for production
eas build --platform ios # sends to App Store
eas build --platform android # generates .apk or .aab
# Build for testing (internal distribution)
eas build --platform all --profile preview
eas.json controls build profiles:
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {}
}
}
EAS Submit can also handle App Store and Play Store submissions:
eas submit --platform ios # submits the latest build to App Store Connect
OTA Updates โ what they can and canโt change
OTA (Over-The-Air) updates push JavaScript bundle changes to users without going through the App Store review process. Both EAS Update (Expoโs solution) and CodePush (Microsoft/App Center) work this way.
What OTA CAN update
- All JavaScript code โ components, business logic, navigation
- Asset files that are bundled in JavaScript (images imported as
require('./icon.png')) - Third-party JS libraries
What OTA CANNOT update
- Native code (Swift, Kotlin, C++) โ changes here always require a new binary
- Native dependencies โ adding or updating a library with native code requires a store release
- App permissions โ declared in
Info.plistandAndroidManifest.xml - App icon, splash screen โ native assets
- The Expo SDK version itself
This is the critical constraint: OTA is for JS hotfixes, not architecture changes.
EAS Update setup
npx expo install expo-updates
eas update:configure
// In your app โ check for updates on launch
import * as Updates from 'expo-updates';
async function checkForUpdates() {
if (!Updates.isEmbeddedLaunch) return; // already on a downloaded update
try {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
await Updates.fetchUpdateAsync();
await Updates.reloadAsync(); // restart to apply
}
} catch (err) {
console.error('Update check failed:', err);
}
}
# Push an update
eas update --branch production --message "Fix login crash"
CodePush (Microsoft App Center)
import CodePush from 'react-native-code-push';
// Wrap the root component โ checks and applies updates automatically
export default CodePush({
checkFrequency: CodePush.CheckFrequency.ON_APP_RESUME,
updateDialog: { title: 'Update Available', optionalInstallButtonLabel: 'Install' },
installMode: CodePush.InstallMode.ON_NEXT_RESUME,
})(App);
The full CI/CD pipeline
Developer pushes code
โ
GitHub Actions / CI โ run tests, type check, lint
โ
EAS Build (on merge to main) โ build iOS + Android binaries
โ
EAS Submit โ upload to TestFlight / Google Play Internal
โ
QA approval
โ
EAS Submit โ production release
โ (for JS-only hotfixes)
EAS Update / CodePush โ push OTA update