Expo Managed Workflow
Expo managed workflow for rapid React Native development with minimal native configuration
You are an expert in the Expo managed workflow for building cross-platform mobile apps with React Native. ## Key Points - Use `npx expo install` instead of `npm install` for Expo-compatible dependency versions. - Keep `app.config.ts` dynamic for environment-specific builds (dev, staging, production). - Use Expo Router for file-based navigation — it aligns with web conventions and supports deep linking out of the box. - Prefer Expo SDK modules (expo-camera, expo-file-system, expo-secure-store) over bare community packages when available, as they are tested against the current SDK version. - Use config plugins instead of ejecting when you need native customization. - Pin your Expo SDK version and upgrade deliberately using `npx expo install --fix`. - Use `expo-constants` and `expo-updates` to manage runtime version checks and feature flags. - **Installing incompatible native packages**: Community packages with native code may not match your Expo SDK version. Always check compatibility or use `npx expo install`. - **Ignoring the prebuild cache**: Running `npx expo prebuild --clean` is necessary after changing config plugins or native dependencies; stale caches cause mysterious build failures. - **Hardcoding secrets in app.config.ts**: Use `eas secret` or environment variables in EAS Build profiles instead of committing secrets. - **Not testing on real devices early**: The Expo Go client lacks many native modules. Use development builds (`expo-dev-client`) for testing native features. - **Skipping TypeScript**: Expo has first-class TypeScript support. Typed routes with Expo Router catch navigation bugs at compile time. ## Quick Example ```bash npx create-expo-app@latest my-app cd my-app npx expo start ```
skilldb get react-native-skills/Expo Managed WorkflowFull skill: 211 linesExpo Managed Workflow — React Native
You are an expert in the Expo managed workflow for building cross-platform mobile apps with React Native.
Core Philosophy
Expo's managed workflow is built on a radical premise: you should not need to touch native code for the vast majority of mobile app features. The android/ and ios/ directories are generated at build time from your app.json/app.config.ts configuration, and the Expo SDK provides JavaScript interfaces to most platform APIs (camera, file system, notifications, biometrics). This is not a limitation -- it is a deliberate trade-off that lets you ship faster by keeping the entire project in one language, with one build system, and one set of dependencies.
When the managed workflow does need native customization, config plugins provide an escape hatch that does not require ejecting. A config plugin is a function that modifies the generated native project at build time -- adding permissions, configuring Info.plist values, or patching build.gradle. This keeps native customizations declarative, version-controlled, and reproducible. Unlike ejecting, which creates a permanent maintenance burden for native directories, config plugins are applied fresh on every build.
Dependency compatibility is the most common source of pain in Expo projects. Every Expo SDK version is tested against a specific set of native dependency versions. Installing a community package that requires a different version of a native library creates build failures that are difficult to diagnose. The npx expo install command resolves this by selecting dependency versions compatible with your current SDK version. Using npm install directly bypasses this compatibility check and is the most frequent cause of "it works in development but fails in EAS Build" issues.
Anti-Patterns
-
Using npm install instead of npx expo install for packages with native dependencies:
npm installgrabs the latest version of a package, which may not be compatible with your Expo SDK version.npx expo installselects the version that is tested and supported, preventing native build failures. -
Hardcoding configuration secrets in app.config.ts: API keys, analytics tokens, and service credentials committed to source control are visible to anyone with repository access. Use
eas secretfor build-time secrets and environment variables in EAS Build profiles for environment-specific configuration. -
Testing only in Expo Go during development: Expo Go is a pre-built client that lacks many native modules and third-party packages. Features that work in Expo Go may fail in a production build, and features that require custom native modules will not work in Expo Go at all. Use development builds (
expo-dev-client) for testing native features early. -
Ignoring prebuild cache after changing config plugins: Running
npx expo prebuildgenerates native projects from your configuration. If you change a config plugin or update a native dependency without runningnpx expo prebuild --clean, the stale cached native project will be used, causing mysterious build failures that do not match your configuration. -
Skipping TypeScript in Expo Router projects: Expo Router supports typed routes that catch navigation bugs at compile time. Without TypeScript, typos in route names and missing parameters are only caught at runtime. Typed routes turn "the screen crashes when you tap that button" into "the code does not compile."
Overview
Expo is a framework and platform built around React Native that simplifies the development lifecycle. The managed workflow abstracts away native code complexity, letting developers focus on JavaScript/TypeScript while Expo handles native builds, OTA updates, and device API access through its SDK.
Expo SDK 51+ uses the "continuous native generation" approach where the android/ and ios/ directories are generated at build time from app.json/app.config.js, removing the need to manage native projects directly.
Core Concepts
Project Initialization
npx create-expo-app@latest my-app
cd my-app
npx expo start
App Configuration with app.config.ts
import { ExpoConfig, ConfigContext } from "expo/config";
export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
name: "My App",
slug: "my-app",
version: "1.0.0",
orientation: "portrait",
icon: "./assets/icon.png",
scheme: "myapp",
splash: {
image: "./assets/splash.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", "expo-secure-store"],
extra: {
eas: { projectId: "your-project-id" },
apiUrl: process.env.API_URL,
},
});
Expo Router (File-Based Routing)
app/
_layout.tsx # Root layout
index.tsx # Home screen (/)
settings.tsx # /settings
(tabs)/
_layout.tsx # Tab navigator layout
home.tsx # /home tab
profile.tsx # /profile tab
[id].tsx # Dynamic route /123
(auth)/
login.tsx # /login (grouped)
// app/_layout.tsx
import { Stack } from "expo-router";
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: "Home" }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="[id]" options={{ title: "Detail" }} />
</Stack>
);
}
Using Expo SDK Modules
import * as ImagePicker from "expo-image-picker";
import * as Location from "expo-location";
import * as Notifications from "expo-notifications";
async function pickImage() {
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (!permission.granted) return;
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
quality: 0.8,
});
if (!result.canceled) {
return result.assets[0].uri;
}
}
Implementation Patterns
Config Plugins for Native Customization
When the managed workflow needs native-level changes, config plugins modify the generated native projects at build time:
// plugins/withCustomAndroidManifest.ts
import { ConfigPlugin, withAndroidManifest } from "expo/config-plugins";
const withCustomPermission: ConfigPlugin = (config) => {
return withAndroidManifest(config, async (config) => {
const manifest = config.modResults.manifest;
manifest["uses-permission"] = manifest["uses-permission"] || [];
manifest["uses-permission"].push({
$: { "android:name": "android.permission.BLUETOOTH_CONNECT" },
});
return config;
});
};
export default withCustomPermission;
Environment-Driven Configuration
// app.config.ts
const IS_DEV = process.env.APP_VARIANT === "development";
const IS_PREVIEW = process.env.APP_VARIANT === "preview";
export default {
name: IS_DEV ? "My App (Dev)" : IS_PREVIEW ? "My App (Preview)" : "My App",
slug: "my-app",
ios: {
bundleIdentifier: IS_DEV
? "com.example.myapp.dev"
: IS_PREVIEW
? "com.example.myapp.preview"
: "com.example.myapp",
},
};
Development Builds
# Install expo-dev-client for custom dev builds
npx expo install expo-dev-client
# Create a development build
eas build --profile development --platform ios
# Start dev server pointing at the dev build
npx expo start --dev-client
Best Practices
- Use
npx expo installinstead ofnpm installfor Expo-compatible dependency versions. - Keep
app.config.tsdynamic for environment-specific builds (dev, staging, production). - Use Expo Router for file-based navigation — it aligns with web conventions and supports deep linking out of the box.
- Prefer Expo SDK modules (expo-camera, expo-file-system, expo-secure-store) over bare community packages when available, as they are tested against the current SDK version.
- Use config plugins instead of ejecting when you need native customization.
- Pin your Expo SDK version and upgrade deliberately using
npx expo install --fix. - Use
expo-constantsandexpo-updatesto manage runtime version checks and feature flags.
Common Pitfalls
- Installing incompatible native packages: Community packages with native code may not match your Expo SDK version. Always check compatibility or use
npx expo install. - Ignoring the prebuild cache: Running
npx expo prebuild --cleanis necessary after changing config plugins or native dependencies; stale caches cause mysterious build failures. - Hardcoding secrets in app.config.ts: Use
eas secretor environment variables in EAS Build profiles instead of committing secrets. - Not testing on real devices early: The Expo Go client lacks many native modules. Use development builds (
expo-dev-client) for testing native features. - Skipping TypeScript: Expo has first-class TypeScript support. Typed routes with Expo Router catch navigation bugs at compile time.
Install this skill directly: skilldb add react-native-skills
Related Skills
Reanimated Animations
High-performance animations in React Native using Reanimated and Gesture Handler
Eas Build Ota Updates
Deploying React Native apps with EAS Build, app store submission, and OTA updates via EAS Update
Native Modules Turbo Modules
Creating native modules and Turbo Modules to bridge platform-specific functionality into React Native
React Navigation Patterns
React Navigation patterns for stack, tab, drawer, and nested navigators in React Native
Offline Storage
Offline storage strategies in React Native using AsyncStorage, MMKV, and WatermelonDB
State Management Zustand Jotai
State management in React Native using Zustand and Jotai for scalable, performant app state