Skip to main content
Technology & EngineeringReact Native211 lines

Expo Managed Workflow

Expo managed workflow for rapid React Native development with minimal native configuration

Quick Summary26 lines
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 lines
Paste into your CLAUDE.md or agent config

Expo 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 install grabs the latest version of a package, which may not be compatible with your Expo SDK version. npx expo install selects 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 secret for 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 prebuild generates native projects from your configuration. If you change a config plugin or update a native dependency without running npx 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 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.

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 --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.

Install this skill directly: skilldb add react-native-skills

Get CLI access →