Expo
Cross-platform mobile with Expo managed workflow. Setup, native features (EAS Build/Update), deployment with TypeScript and React Native.
When to Use
- Cross-platform mobile (iOS + Android)
- Expo managed workflow
- EAS Build/Update config
- Native features via Expo SDK
- OTA updates
Don’t use for:
- Bare React Native (use react-native skill)
- Web-only apps
- Unsupported custom native modules
Critical Patterns
✅ REQUIRED: Use Expo SDK for Native Features
// ✅ CORRECT: Expo SDK modules
import * as Location from "expo-location";
import { Camera } from "expo-camera";
// ❌ WRONG: Direct React Native linking (use Expo modules)
import { NativeModules } from "react-native";
✅ REQUIRED: Handle Permissions Properly
// ✅ CORRECT: Request permissions before using
const { status } = await Location.requestForegroundPermissionsAsync();
if (status === "granted") {
const location = await Location.getCurrentPositionAsync();
}
// ❌ WRONG: No permission check
const location = await Location.getCurrentPositionAsync(); // May crash
✅ REQUIRED: Use TypeScript
// ✅ CORRECT: Typed props and state
interface Props {
title: string;
}
const Component: React.FC<Props> = ({ title }) => {
const [count, setCount] = useState<number>(0);
};
✅ REQUIRED: EAS Build Configuration
Configure build profiles before submitting to app stores.
{
"build": {
"development": { "developmentClient": true, "distribution": "internal" },
"preview": { "distribution": "internal" },
"production": {}
}
}
# Build for all platforms (production)
eas build --platform all --profile production
# Run development build on device
eas build --platform ios --profile development
✅ REQUIRED: Over-the-Air Updates (EAS Update)
Configure OTA updates in app.json. Match runtimeVersion to native changes.
{
"expo": {
"updates": { "url": "https://u.expo.dev/<project-id>" },
"runtimeVersion": { "policy": "appVersion" }
}
}
# Publish JS-only update (no native change needed)
eas update --branch production --message "Fix login crash"
❌ NEVER: Eject Without a Clear Reason
// ❌ WRONG: Ejecting to bare workflow for minor native customization
// Loses: Expo Go dev testing, managed OTA updates, EAS Build simplicity
// ✅ CORRECT: Config plugins handle most native customizations
const { withInfoPlist } = require('@expo/config-plugins');
module.exports = withInfoPlist(config, (config) => {
config.modResults.NSCameraUsageDescription = "For QR scanning";
return config;
});
// Only eject when config plugins cannot satisfy the requirement
Decision Tree
Native feature?
→ Check Expo SDK first
Custom native code?
→ Config plugins or eject
Store builds?
→ EAS Build (cloud)
OTA updates?
→ EAS Update
Platform code?
→ Platform.select() or .ios.tsx/.android.tsx
Testing?
→ Expo Go (dev), devices/simulators (final)
Navigation?
→ Expo Router or React Navigation
Example
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
export default function App() {
return (
<View style={styles.container}>
<Text>Welcome to Expo</Text>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
Edge Cases
-
Platform-specific code: Use
Platform.select()for inline differences; platform-specific file extensions (.ios.tsx,.android.tsx) for large divergences between platforms. -
Permission denial: Always handle the denied state — show an explanation UI and guide the user to device settings. Never assume permission is granted on subsequent launches.
-
OTA update conflicts: JS-only changes publish via
eas update. Any native code change (new SDK module, config plugin change) requires a new app store build with a matchingruntimeVersionbump. -
Offline mode: Use
@react-native-community/netinfoto detect connectivity. Cache data withAsyncStorageor MMKV. Show a visible offline indicator and gracefully block network-dependent actions. -
Custom native modules: Use Expo config plugins first. Use a development client (
expo-dev-client) for modules not available in Expo Go. Only eject to bare workflow when config plugins cannot satisfy the requirement.
Checklist
-
eas initrun — project linked to EAS -
eas.jsonconfigured with development, preview, production profiles - Permissions declared in
app.json(android.permissions,ios.infoPlist) - OTA updates configured (
updates.url,runtimeVersion.policy) - Platform-specific code uses
Platform.select()or platform file extensions - Offline state handled (network detection + cached data fallback)
- TypeScript strict mode enabled
- All native features use Expo SDK modules (not direct React Native linking)
- Expo Go used for dev, physical device/simulator for final testing