Skip to main content

Overview

While Expo provides a comprehensive SDK, sometimes you need custom native code or third-party libraries with native dependencies. Expo’s prebuild workflow makes this possible while maintaining a great developer experience.

Understanding Prebuild

Expo’s prebuild workflow generates the ios and android directories from your app.json configuration:
npx expo prebuild
This creates:
  • ios/ - Xcode project
  • android/ - Android Studio project
Both are gitignored by default and regenerated as needed.
Prebuild is similar to eject but reversible. You can delete the native directories and regenerate them anytime.

When to Use Custom Native Code

  • Adding third-party libraries with native dependencies
  • Implementing features not available in Expo SDK
  • Integrating platform-specific SDKs (payment processors, analytics, etc.)
  • Building custom native modules
  • Modifying native build configuration

Adding Native Libraries

1

Install the library

npm install react-native-example-library
2

Run prebuild

npx expo prebuild
This automatically:
  • Links the native module
  • Updates CocoaPods (iOS)
  • Updates Gradle dependencies (Android)
3

Rebuild the app

npx expo run:ios
npx expo run:android

Example: Adding react-native-maps

npm install react-native-maps
npx expo prebuild
npx expo run:ios
Then use it:
app/map.tsx
import MapView, { Marker } from 'react-native-maps';

export default function MapScreen() {
  return (
    <MapView
      style={{ flex: 1 }}
      initialRegion={{
        latitude: 37.78825,
        longitude: -122.4324,
        latitudeDelta: 0.0922,
        longitudeDelta: 0.0421,
      }}
    >
      <Marker
        coordinate={{ latitude: 37.78825, longitude: -122.4324 }}
        title="San Francisco"
      />
    </MapView>
  );
}

Config Plugins

Config plugins modify the native projects during prebuild without manual editing:
app.json
{
  "expo": {
    "plugins": [
      [
        "react-native-maps",
        {
          "googleMapsApiKey": "YOUR_API_KEY"
        }
      ]
    ]
  }
}

Common plugins

app.json
{
  "expo": {
    "plugins": [
      [
        "react-native-maps",
        {
          "googleMapsApiKey": "AIza...",
          "useGoogleMaps": true
        }
      ]
    ]
  }
}

Creating Custom Native Modules

Using Expo Modules API

The modern way to create native modules:
1

Create a local Expo module

npx create-expo-module@latest --local my-module
This creates:
modules/
└── my-module/
    ├── android/
    ├── ios/
    ├── src/
    └── index.ts
2

Implement iOS module

modules/my-module/ios/MyModule.swift
import ExpoModulesCore

public class MyModule: Module {
  public func definition() -> ModuleDefinition {
    Name("MyModule")

    Function("hello") { (name: String) -> String in
      return "Hello, \(name)!"
    }

    AsyncFunction("fetchData") { (url: String, promise: Promise) in
      URLSession.shared.dataTask(with: URL(string: url)!) { data, response, error in
        if let error = error {
          promise.reject(error)
          return
        }
        if let data = data {
          let text = String(data: data, encoding: .utf8)
          promise.resolve(text)
        }
      }.resume()
    }

    Events("onChange")

    OnCreate {
      // Module initialization
    }
  }
}
3

Implement Android module

modules/my-module/android/src/main/java/expo/modules/mymodule/MyModule.kt
package expo.modules.mymodule

import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition

class MyModule : Module() {
  override fun definition() = ModuleDefinition {
    Name("MyModule")

    Function("hello") { name: String ->
      "Hello, $name!"
    }

    AsyncFunction("fetchData") { url: String ->
      // Fetch data from URL
      val result = fetchDataFromUrl(url)
      result
    }

    Events("onChange")

    OnCreate {
      // Module initialization
    }
  }

  private fun fetchDataFromUrl(url: String): String {
    // Implementation
    return "data"
  }
}
4

Use the module

app/index.tsx
import MyModule from '../modules/my-module';

export default function App() {
  const [data, setData] = useState('');

  useEffect(() => {
    const greeting = MyModule.hello('World');
    console.log(greeting); // "Hello, World!"

    MyModule.fetchData('https://api.example.com/data')
      .then(result => setData(result));

    const subscription = MyModule.addListener('onChange', (event) => {
      console.log('Changed:', event);
    });

    return () => subscription.remove();
  }, []);

  return <Text>{data}</Text>;
}

Module capabilities

Function("multiply") { (a: Int, b: Int) -> Int in
  return a * b
}

AsyncFunction("download") { (url: String, promise: Promise) in
  // Async work
  promise.resolve(result)
}

Creating Config Plugins

Config plugins automate native configuration:
plugins/withCustom.js
const { withDangerousMod } = require('@expo/config-plugins');
const fs = require('fs');
const path = require('path');

function withCustomAndroidPlugin(config) {
  return withDangerousMod(config, [
    'android',
    async (config) => {
      const filePath = path.join(
        config.modRequest.platformProjectRoot,
        'app/src/main/AndroidManifest.xml'
      );

      let contents = fs.readFileSync(filePath, 'utf-8');
      
      // Modify AndroidManifest.xml
      contents = contents.replace(
        '<application',
        '<application\n        android:usesCleartextTraffic="true"'
      );

      fs.writeFileSync(filePath, contents);

      return config;
    },
  ]);
}

module.exports = withCustomAndroidPlugin;
Use it:
app.json
{
  "expo": {
    "plugins": ["./plugins/withCustom.js"]
  }
}

Modifying Native Code Directly

If you need full control:
1

Run prebuild without gitignore

app.json
{
  "expo": {
    "experiments": {
      "noExpoManifestInBuildDir": true
    }
  }
}
npx expo prebuild
# Remove ios/ and android/ from .gitignore
git add ios/ android/
2

Modify native files

Edit ios/ and android/ directories directly in Xcode or Android Studio.
3

Commit changes

git commit -m "Add custom native modifications"
Once you commit native directories, npx expo prebuild will warn you before overwriting changes. You lose some benefits of the managed workflow.

Development Workflow

# Make changes to JS/config
npx expo prebuild --clean
npx expo run:ios

With native directories

# Make changes to native code
npx expo run:ios
# Or open in Xcode/Android Studio

EAS Build with Custom Native Code

Custom native code works seamlessly with EAS Build:
eas.json
{
  "build": {
    "development": {
      "developmentClient": true
    },
    "preview": {
      "distribution": "internal"
    },
    "production": {}
  }
}
Build:
eas build --platform ios --profile production
EAS Build runs npx expo prebuild automatically.

Troubleshooting

# Clean and rebuild
npx expo prebuild --clean
npx expo run:ios
For iOS specifically:
cd ios && pod install && cd ..
npx expo run:ios
  • Check minimum iOS/Android version requirements
  • Verify the library is compatible with React Native version
  • Look for config plugin in library documentation
  • Check for peer dependency warnings
npx expo prebuild --clean
The --clean flag removes and regenerates native directories.
# Clear Metro cache
npx expo start --clear

# Clear native build cache
cd ios && rm -rf build && cd ..
cd android && ./gradlew clean && cd ..

Best Practices

  • Use config plugins: Automate native configuration instead of manual editing
  • Stay in managed workflow: Keep native directories gitignored when possible
  • Document native dependencies: List libraries with native code in README
  • Test on both platforms: Native code behavior can differ
  • Use Expo Modules API: Prefer it over React Native’s legacy bridge
  • Version lock native dependencies: Specify exact versions to avoid breaking changes
  • Clean rebuilds: Run npx expo prebuild --clean when in doubt
  • Check compatibility: Verify libraries work with your Expo SDK version

Migration from Bare Workflow

If you have existing native directories:
1

Backup native changes

git commit -m "Backup before migration"
2

Remove native directories

rm -rf ios/ android/
3

Add plugins for native changes

Convert manual native modifications to config plugins in app.json.
4

Regenerate with prebuild

npx expo prebuild
5

Test thoroughly

npx expo run:ios
npx expo run:android