Native Modules Basics
Ten examples for when JavaScript-only solutions fail and you must cross the native boundary - the foundation every Expo SDK 57 team needs before adding third-party libraries, config plugins, or custom modules.
Search across all documentation pages
Ten examples for when JavaScript-only solutions fail and you must cross the native boundary - the foundation every Expo SDK 57 team needs before adding third-party libraries, config plugins, or custom modules.
Start from a blank Expo app with a development build path ready - native modules do not run in Jest without mocks and cannot load arbitrary code in Expo Go:
npx create-expo-app@latest NativeBasics --template blank-typescript@sdk-57 --yes
cd NativeBasics
npx expo install expo-dev-client expo-modules-coreTooling: These examples target Expo SDK 57 (
expo~57.0.4), React Native 0.86.0, and React 19.2.3. Pair this page with Native Module Rules before adding any package with native code.
Camera frames, GPS fixes, accelerometer streams, and biometric sensors read device hardware through OS APIs. JavaScript cannot access them without a native module:
import { CameraView, useCameraPermissions } from "expo-camera";
import { Button } from "react-native";
export function ScanScreen() {
const [permission, requestPermission] = useCameraPermissions();
if (!permission?.granted) {
return <Button title="Allow camera" onPress={requestPermission} />;
}
return <CameraView style={{ flex: 1 }} facing="back" />;
}expo-camera wraps AVFoundation (iOS) and CameraX (Android) - no pure-JS substituteexpo-camera; arbitrary camera SDKs do notAsyncStorage is unencrypted SQLite. Tokens, refresh secrets, and PII belong in Keychain (iOS) or Keystore (Android):
import * as SecureStore from "expo-secure-store";
export async function saveRefreshToken(token: string): Promise<void> {
await SecureStore.setItemAsync("refresh_token", token, {
keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
}
export async function loadRefreshToken(): Promise<string | null> {
return SecureStore.getItemAsync("refresh_token");
}localStorage patterns do not applyexpo-secure-store - see Mocking Native ModulesTimers and fetch pause when the app is backgrounded or killed. Push delivery, location updates, and background uploads need native schedulers:
import * as TaskManager from "expo-task-manager";
import * as Location from "expo-location";
const TASK_NAME = "background-location";
TaskManager.defineTask(TASK_NAME, ({ data, error }) => {
if (error) return;
const { locations } = data as { locations: Location.LocationObject[] };
// Upload locations to your API - native task keeps firing
});
export async function startBackgroundLocation(): Promise<void> {
await Location.startLocationUpdatesAsync(TASK_NAME, {
accuracy: Location.Accuracy.Balanced,
distanceInterval: 100,
showsBackgroundLocationIndicator: true,
});
}Push is not HTTP polling. Device tokens come from Apple Push Notification service and Firebase Cloud Messaging native SDKs:
import * as Notifications from "expo-notifications";
import * as Device from "expo-device";
export async function registerPushToken(): Promise<string | null> {
if (!Device.isDevice) return null;
const { status } = await Notifications.requestPermissionsAsync();
if (status !== "granted") return null;
const token = await Notifications.getExpoPushTokenAsync();
return token.data;
}Some views cannot be faithfully reproduced in React Native layout - maps, video players, PDF renderers, and WebViews embed platform-native views inside the RN tree:
import MapView, { Marker } from "react-native-maps";
export function StoreMap({ lat, lng }: { lat: number; lng: number }) {
return (
<MapView
style={{ flex: 1 }}
initialRegion={{
latitude: lat,
longitude: lng,
latitudeDelta: 0.02,
longitudeDelta: 0.02,
}}
>
<Marker coordinate={{ latitude: lat, longitude: lng }} title="Store" />
</MapView>
);
}npm installHealthKit, NFC, CarPlay, Siri shortcuts, and Android intents exist only on one platform. Feature detection gates JS; native modules implement the call:
import { Platform } from "react-native";
import * as LocalAuthentication from "expo-local-authentication";
export async function biometricGate(): Promise<boolean> {
if (Platform.OS === "web") return false;
const compatible = await LocalAuthentication.hasHardwareAsync();
if (!compatible) return false;
const enrolled = await LocalAuthentication.isEnrolledAsync();
if (!enrolled) return false;
const result = await LocalAuthentication.authenticateAsync({
promptMessage: "Unlock to continue",
});
return result.success;
}Platform.OS checks prevent crashes; they do not replace native implementationPlatform.select inside servicesHeavy image transforms, video encoding, cryptography, and ML inference block the JS thread if done in pure JavaScript. Native modules run work on background queues or GPU paths:
import * as ImageManipulator from "expo-image-manipulator";
export async function resizeAvatar(uri: string): Promise<string> {
const result = await ImageManipulator.manipulateAsync(
uri,
[{ resize: { width: 256 } }],
{ compress: 0.8, format: ImageManipulator.SaveFormat.JPEG }
);
return result.uri;
}Share sheets, home-screen widgets, App Clips, and universal links register at the native app layer. JavaScript receives deep-link events; native code owns registration:
import * as Linking from "expo-linking";
export function subscribeToDeepLinks(onUrl: (url: string) => void): () => void {
const sub = Linking.addEventListener("url", ({ url }) => onUrl(url));
Linking.getInitialURL().then((url) => {
if (url) onUrl(url);
});
return () => sub.remove();
}// app.json excerpt - associated domains are native entitlements
{
"expo": {
"scheme": "myapp",
"ios": {
"associatedDomains": ["applinks:shop.example.com"]
}
}
}Info.plist editsWeb Bluetooth is unavailable on React Native. BLE scanning, pairing, and GATT read/write require CoreBluetooth (iOS) and BluetoothGatt (Android):
// Conceptual - real BLE libraries expose native modules
import { BleManager } from "react-native-ble-plx";
const manager = new BleManager();
export async function scanForPeripherals(): Promise<void> {
manager.startDeviceScan(null, null, (error, device) => {
if (error) return;
if (device?.name) console.log("Found", device.name);
});
}expo-dev-client from day oneSometimes the gap is not capability but compliance: certificate pinning, jailbreak detection, proprietary payment SDKs, or MDM-required APIs. These ship as native binaries with no Expo SDK equivalent:
// App-layer facade - implementation is a native module or third-party SDK
export type AttestationResult = { passed: boolean; reason?: string };
export async function attestDevice(): Promise<AttestationResult> {
// Native module: DeviceCheck (iOS) / Play Integrity (Android)
const NativeAttestation = require("../native/attestation").default;
return NativeAttestation.runAsync();
}JavaScript (Hermes)
│
▼
expo-modules-core / JSI host object
│
├── Expo Modules API (expo-camera, expo-file-system, …)
├── React Native TurboModules (community libraries)
└── Legacy bridge fallback (older libraries only)
│
▼
Swift / Kotlin / C++ native implementation
| Layer | What you import | Rebuild required? |
|---|---|---|
| Expo SDK module | import * as Camera from "expo-camera" | Yes, after install |
| Community native lib | import MapView from "react-native-maps" | Yes, after install |
| Custom Expo module | import MyModule from "my-local-module" | Yes, always |
| JS-only package | import { z } from "zod" | No - OTA can ship |
import { requireNativeModule, requireOptionalNativeModule } from "expo-modules-core";
// Throws if native class missing from binary
const FileSystem = requireNativeModule("ExpoFileSystem");
// Returns null in tests or when module not linked
const Optional = requireOptionalNativeModule("ExpoCustomFeature");See Expo Modules Core for bootstrap details and Expo Go vs Development Builds for runtime constraints.
npx expo install expo-dev-client and build a development client.npm install instead of npx expo install - JS and native binary versions drift. Fix: Always npx expo install <package> for native deps.package.json change - Metro hot reload does not compile Swift/Kotlin. Fix: npx expo run:ios / eas build after every native change.jest-expo.ios/ and android/ in CNG apps - next prebuild --clean wipes edits. Fix: Encode changes in config plugins per CNG prebuild.A bridge between JavaScript and platform code (Swift, Kotlin, Objective-C, Java, C++). On React Native 0.86 with New Architecture, most modules use JSI and TurboModules for synchronous, typed calls.
No - you consume them from JavaScript. The Expo team maintains the native implementation. You only write native code when no SDK or community package fits.
No. OTA (EAS Update) ships JavaScript and assets only. Native dependency changes require a new store binary or development build.
Expo Go: SDK modules only, fastest spike. Development build: your package.json native deps, required for third-party and custom modules.
Look for ios/, android/, .podspec, or expo-module.config.json in the package. Run npx expo-doctor after install - it flags incompatible native deps.
Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026