mobile-framework-expo · git:20260202.b236384 · 2026-02-02 · sha256 87134b43ac679f8b
mobile-framework-expo git:20260202.b236384A
Immutable. This exact content is served forever at /api/v1/blob/87134b43ac679f8b.
---
name: mobile-framework-expo
description: Expo managed workflow
---
# Expo Development Patterns
> **Quick Guide:** Build production-ready React Native apps with Expo. Use managed workflow with Continuous Native Generation for most projects, Expo Router for file-based navigation, and EAS for builds/updates. Development builds replace Expo Go for production testing.
---
<critical_requirements>
## CRITICAL: Before Using This Skill
> **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
**(You MUST use development builds for production testing - Expo Go is for prototyping only)**
**(You MUST update runtimeVersion when making native dependency changes to prevent OTA update crashes)**
**(You MUST use config plugins for native customization - NEVER manually edit android/ios directories in managed workflow)**
**(You MUST use EXPO*PUBLIC* prefix for client-side environment variables - NEVER store secrets in these variables)**
</critical_requirements>
---
**Auto-detection:** Expo, expo-router, EAS Build, EAS Update, expo-dev-client, app.config.js, app.json, expo prebuild, npx expo, eas.json, expo-constants, expo-notifications, Continuous Native Generation, CNG
**When to use:**
- Starting new React Native projects with rapid development needs
- Building apps that need OTA (over-the-air) updates
- Using file-based routing similar to Next.js
- Managing native code without maintaining android/ios directories
- Deploying to app stores with cloud builds
**Key patterns covered:**
- Managed workflow with Continuous Native Generation (CNG)
- Expo Router file-based navigation
- EAS Build, Submit, and Update workflows
- Development builds vs Expo Go
- Config plugins for native customization
- Environment configuration and secrets
- Push notifications setup
**When NOT to use:**
- Apps requiring complex custom native code beyond Expo Modules API
- When app size must be under 15MB (Expo adds overhead)
- Legacy React Native projects not ready for migration
**Detailed Resources:**
- For code examples, see [examples/](examples/) folder
- For decision frameworks and anti-patterns, see [reference.md](reference.md)
---
<philosophy>
## Philosophy
Expo transforms React Native development from "write once, debug everywhere" to "write once, deploy confidently." The key insight is that **most apps don't need direct native access** - they need well-maintained native modules with consistent APIs.
**Core principles:**
1. **Managed by default** - Let Expo handle native complexity; prebuild only when necessary
2. **Continuous Native Generation** - Treat android/ios as build artifacts, not source code
3. **Development builds for truth** - Expo Go is for learning; development builds show production reality
4. **OTA for velocity** - Ship JavaScript updates without app store delays
5. **Config plugins over ejection** - Customize native code declaratively when needed
**Mental model:**
Expo is NOT a limitation on React Native - it's a professional-grade abstraction. You can always drop down to native code via Expo Modules API or prebuild, but most apps never need to.
</philosophy>
---
<patterns>
## Core Patterns
### Pattern 1: Project Configuration
Configure Expo projects using `app.json` (static) or `app.config.js` (dynamic).
#### Static Configuration
```typescript
// app.json - Static configuration
{
"expo": {
"name": "MyApp",
"slug": "my-app",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.example.myapp"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.example.myapp"
},
"plugins": [
"expo-router"
]
}
}
```
#### Dynamic Configuration
```typescript
// app.config.ts - Dynamic configuration with TypeScript
import type { ExpoConfig, ConfigContext } from "expo/config";
const IS_PRODUCTION = process.env.APP_ENV === "production";
const APP_VERSION = "1.0.0";
const BUILD_NUMBER = 1;
export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
name: IS_PRODUCTION ? "MyApp" : "MyApp (Dev)",
slug: "my-app",
version: APP_VERSION,
ios: {
supportsTablet: true,
bundleIdentifier: IS_PRODUCTION
? "com.example.myapp"
: "com.example.myapp.dev",
buildNumber: String(BUILD_NUMBER),
},
android: {
package: IS_PRODUCTION ? "com.example.myapp" : "com.example.myapp.dev",
versionCode: BUILD_NUMBER,
adaptiveIcon: {
foregroundImage: "./assets/adaptive-icon.png",
backgroundColor: "#ffffff",
},
},
extra: {
eas: {
projectId: process.env.EAS_PROJECT_ID,
},
},
});
```
**Why good:** Named constants prevent magic numbers, dynamic config enables environment-specific builds, TypeScript provides type safety
---
### Pattern 2: Config Plugins for Native Customization
Modify native code declaratively without maintaining android/ios directories.
```typescript
// app.config.ts - Config plugins
import type { ExpoConfig } from "expo/config";
const IOS_DEPLOYMENT_TARGET = "15.1";
const ANDROID_COMPILE_SDK = 35;
const ANDROID_TARGET_SDK = 35;
const ANDROID_MIN_SDK = 24;
export default (): ExpoConfig => ({
name: "MyApp",
slug: "my-app",
plugins: [
// Camera with custom permission message
[
"expo-camera",
{
cameraPermission:
"Allow $(PRODUCT_NAME) to access your camera for photos.",
microphonePermission:
"Allow $(PRODUCT_NAME) to access your microphone for video.",
},
],
// Build properties for SDK versions
[
"expo-build-properties",
{
android: {
compileSdkVersion: ANDROID_COMPILE_SDK,
targetSdkVersion: ANDROID_TARGET_SDK,
minSdkVersion: ANDROID_MIN_SDK,
},
ios: {
deploymentTarget: IOS_DEPLOYMENT_TARGET,
},
},
],
// Location with background permission
[
"expo-location",
{
locationAlwaysAndWhenInUsePermission:
"Allow $(PRODUCT_NAME) to use your location for navigation.",
},
],
],
});
```
**Why good:** Declarative native configuration survives `expo prebuild --clean`, config plugins compose, permissions are explicit
---
### Pattern 3: Environment Variables
Use `EXPO_PUBLIC_` prefix for client-side variables.
```typescript
// .env
EXPO_PUBLIC_API_URL=https://api.example.com
EXPO_PUBLIC_SENTRY_DSN=https://xxxx@sentry.io/xxxx
// .env.local (gitignored - local overrides)
EXPO_PUBLIC_API_URL=http://localhost:3000
```
```typescript
// config/env.ts - Type-safe environment access
const API_URL = process.env.EXPO_PUBLIC_API_URL;
const SENTRY_DSN = process.env.EXPO_PUBLIC_SENTRY_DSN;
// IMPORTANT: These patterns DON'T work - Metro requires static analysis
// const { EXPO_PUBLIC_API_URL } = process.env; // BAD
// process.env['EXPO_PUBLIC_API_URL']; // BAD
if (!API_URL) {
throw new Error("EXPO_PUBLIC_API_URL is required");
}
export const env = {
apiUrl: API_URL,
sentryDsn: SENTRY_DSN,
} as const;
```
```typescript
// Using expo-constants for runtime config
import Constants from "expo-constants";
// Access extra config from app.config.ts
const projectId = Constants.expoConfig?.extra?.eas?.projectId;
const environment = Constants.expoConfig?.extra?.environment;
```
**Why good:** Type-safe access with validation, clear separation of public/private config, build-time substitution for security
---
### Pattern 4: Development Builds
Create custom development builds with `expo-dev-client`.
```bash
# Install dev client
npx expo install expo-dev-client
# Create development build (cloud)
eas build --profile development --platform ios
eas build --profile development --platform android
# Create development build (local)
npx expo run:ios
npx expo run:android
```
```typescript
// eas.json - Development build configuration
{
"cli": {
"version": ">= 7.0.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"simulator": true
},
"android": {
"buildType": "apk"
}
},
"development-device": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"simulator": false
}
}
}
}
```
**Why good:** Development builds include your native dependencies, support push notifications and deep links, allow testing app icons and splash screens
---
### Pattern 5: Asset Management
Load fonts and images efficiently.
```typescript
// app/_layout.tsx - Font loading with splash screen
import { useFonts } from "expo-font";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
import { Stack } from "expo-router";
const SPLASH_HIDE_DELAY_MS = 0;
// Prevent splash screen from auto-hiding
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [fontsLoaded, fontError] = useFonts({
"Inter-Regular": require("../assets/fonts/Inter-Regular.ttf"),
"Inter-Medium": require("../assets/fonts/Inter-Medium.ttf"),
"Inter-Bold": require("../assets/fonts/Inter-Bold.ttf"),
});
useEffect(() => {
if (fontsLoaded || fontError) {
SplashScreen.hideAsync();
}
}, [fontsLoaded, fontError]);
if (!fontsLoaded && !fontError) {
return null;
}
return <Stack />;
}
```
```typescript
// Using expo-image for optimized image loading
import { Image } from "expo-image";
const BLUR_HASH = "L6PZfSi_.AyE_3t7t7R**0o#DgR4";
const IMAGE_TRANSITION_MS = 200;
interface OptimizedImageProps {
uri: string;
width: number;
height: number;
}
export function OptimizedImage({ uri, width, height }: OptimizedImageProps) {
return (
<Image
source={{ uri }}
placeholder={BLUR_HASH}
contentFit="cover"
transition={IMAGE_TRANSITION_MS}
cachePolicy="memory-disk"
style={{ width, height }}
/>
);
}
```
**Why good:** SplashScreen prevents flash of unstyled content, expo-image provides caching and blur hash placeholders, named constants for configuration
</patterns>
---
<integration>
## Integration Guide
**Expo works with your existing mobile development knowledge:**
**Navigation:**
- Use Expo Router for file-based routing (Next.js-like experience)
- See [examples/router.md](examples/router.md) for patterns
**Build and Deploy:**
- Use EAS Build for cloud compilation
- Use EAS Submit for app store submission
- Use EAS Update for OTA updates
- See [examples/eas.md](examples/eas.md) for workflows
**Native Code:**
- Use config plugins for most native customization
- Use Expo Modules API for custom native modules
- Prebuild to bare workflow only when absolutely necessary
**State Management:**
- Expo works with any React state solution
- Use AsyncStorage for persistent client storage
</integration>
---
<critical_reminders>
## CRITICAL REMINDERS
> **All code must follow project conventions in CLAUDE.md**
**(You MUST use development builds for production testing - Expo Go is for prototyping only)**
**(You MUST update runtimeVersion when making native dependency changes to prevent OTA update crashes)**
**(You MUST use config plugins for native customization - NEVER manually edit android/ios directories in managed workflow)**
**(You MUST use EXPO*PUBLIC* prefix for client-side environment variables - NEVER store secrets in these variables)**
**Failure to follow these rules will cause OTA update crashes, broken builds, and security vulnerabilities.**
</critical_reminders>