Integrated approach for adding Expo to native app
The integrated approach adds React Native and Expo to an existing native app by integrating them as a regular library rather than packaging them as a black box. This allows incremental adoption, one screen or view at a time, without requiring a complete rewrite.
Prerequisites for Expo brownfield integration
Node.js LTS, Yarn, and CocoaPods (for iOS) are required. Node.js and Yarn are needed to run JavaScript code and Expo CLI. CocoaPods is required as a dependency management system for iOS and is installed via Ruby.
Create Expo project command for brownfield
Create an Expo project using: npm: `npx create-expo-app@latest my-project --template default@sdk-57`, yarn: `yarn create expo-app my-project --template default@sdk-57`, pnpm: `pnpm create expo-app my-project --template default@sdk-57`, or bun: `bun create expo my-project --template default@sdk-57`. This creates a new directory with your Expo project including an example TypeScript application.
Project structure for brownfield: android and ios directories
Place native code in android and ios directories at the root of your Expo project. Android projects go in my-project/android and iOS projects go in my-project/ios.
Monorepo setup for custom folder structure
If you cannot move native projects to android and ios directories, set up a Yarn monorepo by creating a package.json at the project root with content: {"version": "1.0.0", "private": true, "workspaces": ["my-project"]}. Then run yarn install. This ensures node_modules are at the root and native scripts can interact with React Native code. Custom project root configuration is required in Gradle/CocoaPods.
Android settings.gradle configuration for Expo
Configure settings.gradle with pluginManagement that loads React Native Gradle Plugin and Expo autolinking plugin. Use exec to resolve paths to @react-native/gradle-plugin and expo-modules-autolinking. Configure extensions with com.facebook.react.ReactSettingsExtension and expo-autolinking. Include plugins: id("com.facebook.react.settings") and id("expo-autolinking-settings"). Call expoAutolinking.useExpoVersionCatalog() and expoAutolinking.useExpoModules().
Android build.gradle top-level configuration
The top-level build.gradle must include plugins and configurations to make React Native Gradle and Expo plugins available and applied to the project.
Android app/build.gradle configuration
The app/build.gradle file must include Expo-specific configuration. If using a custom folder structure, adjust the projectRoot value to point to the root of your Expo project.
Android gradle.properties required settings
Add to gradle.properties: reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64, newArchEnabled=true, hermesEnabled=true.
Android INTERNET permission required
Add the INTERNET permission to AndroidManifest.xml to allow network access.
Android debug manifest cleartext traffic for Metro
Enable cleartext traffic in the debug AndroidManifest.xml to allow the app to communicate with the local Metro bundler via HTTP.
Android MainApplication initialization for React Native
Update your Application class to initialize React Native. Reference the bare minimum template's MainApplication.kt for implementation details.
Android ReactActivity implementation
Create a new Activity extending ReactActivity that hosts React Native code. The activity is responsible for starting the React Native runtime and rendering React components. The getMainComponentName() method should return "main". The createReactActivityDelegate() method should return a ReactActivityDelegate using DefaultReactActivityDelegate wrapped with ReactActivityDelegateWrapper, passing BuildConfig.IS_NEW_ARCHITECTURE_ENABLED and fabricEnabled flags.
Android activity theme configuration
Set the theme of ReactActivity to Theme.AppCompat.Light.NoActionBar or another non-ActionBar theme in AndroidManifest.xml to avoid rendering an ActionBar on top of the React Native screen.
iOS Podfile configuration for Expo
Create or update Podfile with: require expo autolinking and react_native_pods scripts; platform :ios, '16.4'; call prepare_react_native_project!(); use target with use_expo_modules!(); configure react-native-config using expo-modules-autolinking; call use_native_modules!() with config command; call use_react_native!() with path to react-native, hermes_enabled: true, app_path, and privacy_file_aggregation_enabled: true; include post_install hook with react_native_post_install.
iOS pod install command
Run `pod install` after configuring the Podfile to integrate React Native code into the app, allowing iOS files to import React Native headers.
iOS Xcode workspace requirement
After pod install, CocoaPods creates a {Project}.xcworkspace file. Open the xcworkspace rather than the traditional xcodeproj. Use `xed my-project/ios` to open the workspace.
iOS ENABLE_USER_SCRIPT_SANDBOXING build setting
In Xcode Build Settings, search for ENABLE_USER_SCRIPT_SANDBOXING and set its value to No. This is required to properly switch between Debug and Release versions of the Hermes engine shipped with React Native.
iOS Build Phases script for JavaScript bundling
Add a Run Script Phase in Build Phases before [CP] Embed Pods Frameworks. This script sources .xcode.env and .xcode.env.local, sets PROJECT_ROOT (one level up from ios directory), skips bundling in Debug, resolves ENTRY_FILE using expo/scripts/resolveAppEntry, resolves CLI_PATH to Expo CLI, sets BUNDLE_COMMAND to "export:embed" by default, sources .xcode.env.updates and .xcode.env.local for overrides, and runs react-native-xcode.sh. The script bundles JavaScript code and assets into the iOS application.
iOS Info.plist UIViewControllerBasedStatusBarAppearance setting
Add the UIViewControllerBasedStatusBarAppearance key with value NO to Info.plist to ensure the status bar is properly managed by React Native.
iOS ReactViewController implementation
Create ReactViewController.swift extending UIViewController. In viewDidLoad, initialize a ReactNativeDelegate and RCTReactNativeFactory, set the factory delegate's dependencyProvider to RCTAppDependencyProvider(), and set the view using rootViewFactory.view(withModuleName: "HelloWorld"). The ReactNativeDelegate extends RCTDefaultReactNativeFactoryDelegate, overrides sourceURL to call bundleURL(), and overrides bundleURL() to return the Metro entry in Debug using RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry") or the bundled main.jsbundle in Release.
iOS presenting React Native view modally
To present React Native view, create a ViewController with a button that instantiates and presents ReactViewController modally. Use UIButton with an action to present the ReactViewController when tapped.
Starting Metro bundler for brownfield development
Run `npm run start` (npm), `yarn run start` (yarn), `pnpm run start` (pnpm), or `bun run start` (bun) in the React Native directory to start the Metro bundler. Metro builds TypeScript code into a bundle, serves it via HTTP on localhost, and enables hot reloading. JavaScript code loads from the development server when reaching the React-powered Activity.
Metro Bundler setup for brownfield debug builds
To use the app with Metro Bundler in debug mode, run 'yarn start' in the app directory and use artifacts built in 'Debug' or 'All' build types. These build types are only supported for Android.
expo-brownfield build command for Android
To build Android brownfield artifacts as reusable packages, use the command: npx expo-brownfield build:android --repo MavenLocal --all --verbose
expo-brownfield build commands for iOS
To build iOS brownfield artifacts, use either 'npx expo-brownfield build:ios --release --verbose' for release builds or 'npx expo-brownfield build:ios --debug --verbose' for debug builds.
expo-brownfield test app purpose
The brownfield-tester app is used for testing the expo-brownfield package and its integration with other packages like expo-router, expo-updates, and expo-dev-menu. It also serves as the base app for expo-brownfield E2E tests.
isolated directory in Brownfield Tester
The isolated directory contains standalone native Android and iOS apps that consume pre-built brownfield artifacts as Maven packages and xcframeworks. They are fully self-contained and do not depend on the monorepo at build time, making this the recommended distribution approach.
Brownfield Tester App structure and purpose
The Brownfield Tester App is a sample application used to test brownfield integration with Expo modules. It contains three main directories: expo-app, integrated, and isolated.
expo-app directory in Brownfield Tester
The expo-app directory contains the React Native / Expo app that serves as the JavaScript source and brownfield artifact builder.
integrated directory in Brownfield Tester
The integrated directory contains native Android and iOS apps that integrate directly with the monorepo using Expo autolinking. They point their project root to expo-app/ and resolve modules at build time.
Brownfield Tester Integrated uses Expo autolinking
The Brownfield Tester Integrated app integrates directly with the Expo monorepo using Expo autolinking. It points its build and project root to ../expo-app, which means React Native modules are resolved and linked at build time from the monorepo's node_modules.
Brownfield Tester Integrated project setup is invalid and should not be replicated
The Brownfield Tester Integrated app redirects its build and project root to ../expo-app. This is an invalid project setup that should not be replicated to other tests or E2E setups and needs to be refactored.
Android Brownfield Tester initialization and Java 17 configuration
The Android app was initialized by creating a new Empty Activity project in Android Studio 2025.1.3 and following the Brownfield Integration guide to integrate Expo modules. Due to React Native targeting Java 17, the default compileOptions were removed from app/build.gradle.kts and dependencyResolutionManagement repositoriesMode was removed from settings.gradle.kts because the react-native plugin configures the maven repo.
iOS Brownfield Tester initialization and Swift 6 configuration
The iOS app was initialized by creating a new SwiftUI project in Xcode 26 and following the Brownfield Integration guide to integrate Expo modules. Due to Swift 6 not being totally supported yet, it was necessary to set Default Actor isolation to nonisolated in the project settings.
Brownfield isolated apps use pre-built artifacts, not Expo autolinking
Brownfield isolated tester apps consume pre-built brownfield artifacts (Maven local repository for Android, xcframeworks via Swift Package for iOS). They do not use Expo autolinking or depend on the monorepo's node_modules at build time — they are fully self-contained. This is the recommended approach for distributing brownfield integrations to existing native apps.
Generate brownfield artifacts for Android with Maven Local
To build Android brownfield artifacts, run: npx expo prebuild --clean -p android, then npx expo-brownfield build:android --repo MavenLocal --all --verbose. This builds and publishes to MavenLocal.
Generate brownfield artifacts for iOS with xcframeworks
To build iOS brownfield artifacts, run: npx expo prebuild --clean -p ios, then npx expo-brownfield build:ios --release --verbose and npx expo-brownfield build:ios --debug --verbose. This builds xcframeworks.
Android brownfield app resolves libraries from mavenLocal()
Once artifacts are published to Maven Local, open the android/ project in Android Studio and build normally. The app resolves the brownfield libraries from mavenLocal().
iOS brownfield app uses xcframeworks via Swift Package
After building xcframeworks, the add_xcframeworks.rb script (in the E2E scripts) adds the Swift Package containing the frameworks to the Xcode project. You can also add them manually via Xcode's 'Add Package Dependencies' with a local package path.