expo
Expo is an open-source platform on top of React Native for building Android, iOS, and web apps from one JavaScript or TypeScript codebase. The expo package is the SDK and runtime; the same monorepo ships the CLI, Expo Router for file-based navigation, the Modules API for native code, and the Expo Go sandbox app. It pairs with EAS, Expo's hosted build, submit, and update service, which is where most of the ship-to-store convenience actually lives.
The default way to build React Native apps and the fastest zero-to-device experience in mobile. Go in knowing the comfortable shipping path routes through EAS and that SDK upgrades are recurring scheduled work, not a one-time cost.
Use it if
- You want a new React Native app running on a real device in minutes, with routing, TypeScript, and a large set of prebuilt native modules preconfigured
- Your team is mostly web developers; the dev server plus Expo Go gives fast iteration without touching Xcode or Android Studio for JS-only work
- You want cloud builds and over-the-air JS updates through EAS instead of maintaining your own CI with macOS runners
- You need one codebase targeting Android, iOS, and web
- Your app depends on custom native code from day one; you will be making development builds constantly and the Expo Go shortcut stops applying
- You want to avoid coupling to a vendor's hosted services: core Expo is MIT, but the smooth build-submit-update path runs through EAS, a commercial platform
- You need the newest React Native release the week it ships; each Expo SDK pins a specific React Native version and you upgrade on Expo's cadence
- Your product is web-only; plain React with Vite is far less machinery for the same result
Setup reality
npx create-expo-app to a running app on your phone via Expo Go is the easiest start in mobile, full stop. The honest parts come later: adding any library with custom native code forces a development build (npx expo run:ios needs a Mac with Xcode, or you use EAS Build in the cloud with an Expo account), SDK upgrades arrive several times a year and each one is a real chore across native config and dependencies, and version mismatches are common enough that npx expo install exists specifically to pin packages to versions your SDK supports.
Patterns
Create and run a new appcreate-app
npx create-expo-app@latest my-app
cd my-app
npx expo startScan the QR code with Expo Go on your phone; the template already includes Expo Router and TypeScript.
Install an SDK-compatible packageinstall-compatible-package
npx expo install expo-camera expo-locationAlways use expo install instead of npm install for Expo and React Native packages; it pins versions known to work with your SDK.
Add a screen with Expo Routerfile-based-routing
// app/index.tsx
import { Link } from 'expo-router';
import { Text, View } from 'react-native';
export default function Home() {
return (
<View>
<Text>Home</Text>
<Link href="/about">About</Link>
</View>
);
}
// app/about.tsx
export default function About() {
return <Text>About</Text>;
}The file path under app/ is the route; no navigator registration code needed.
Run a development build locallydevelopment-build
npx expo run:ios # needs a Mac with Xcode
npx expo run:android # needs Android Studio / SDKRequired as soon as you add a library with custom native code; Expo Go only contains the modules that ship with the SDK.
Use environment variablesenvironment-variables
# .env
EXPO_PUBLIC_API_URL=https://api.example.com
// anywhere in app code
const url = process.env.EXPO_PUBLIC_API_URL;EXPO_PUBLIC_ values are inlined into the client bundle at build time; never put secrets in them.
Configure app identity in app.jsonapp-config
{
"expo": {
"name": "My App",
"slug": "my-app",
"ios": { "bundleIdentifier": "com.example.myapp" },
"android": { "package": "com.example.myapp" }
}
}Changes to native config only take effect in a new build, not through a JS reload or an OTA update.
Ask for camera permission and show a previewcamera-permission
import { CameraView, useCameraPermissions } from 'expo-camera';
import { Button, Text, View } from 'react-native';
export default function Scanner() {
const [permission, requestPermission] = useCameraPermissions();
if (!permission) return <View />;
if (!permission.granted) {
return <Button title="Allow camera" onPress={requestPermission} />;
}
return <CameraView style={{ flex: 1 }} />;
}iOS also requires a usage description string in app.json (via the expo-camera config plugin) or the app gets rejected.
Load a custom fontload-fonts
import { useFonts } from 'expo-font';
export default function Root() {
const [loaded] = useFonts({
Inter: require('./assets/fonts/Inter-Regular.ttf'),
});
if (!loaded) return null;
return <App />;
}Fonts load async at runtime with useFonts; for zero-flash embedding at build time use the expo-font config plugin instead.
Build in the cloud with EAScloud-build
npm install -g eas-cli
eas login
eas build --platform iosNeeds an Expo account and your app store credentials; this is the path that avoids owning a Mac for iOS builds.
Upgrade to a new SDK versionupgrade-sdk
npx expo install expo@latest
npx expo install --fixexpo install --fix realigns every dependency to the new SDK; upgrade one SDK version at a time and read that release's changelog.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| react-native | npm | Bare React Native when you need full control of the native iOS and Android projects |
| @capacitor/core | npm | Wrap an existing web app in a native shell instead of rendering native views |
| nativescript | npm | Native UI from JavaScript without React in the stack |