Mobile Dev Expo Router EAS Build Native Modules 2026

Expo React Native mit Claude Code: Mobile Apps 2026

Expo für plattformübergreifende React Native Apps — Claude Code baut Expo Router Navigation, NativeWind Styling und EAS-Deployment für iOS und Android.

📅 6. Mai 2026 ⏱ 12 min Lesezeit 📱 Mobile & React Native

📱 Was du in diesem Guide lernst

Dieser Guide zeigt, wie Claude Code mit Expo und React Native zusammenarbeitet, um professionelle Mobile Apps für iOS und Android zu entwickeln — von Null bis App Store.

  • create-expo-app
  • Expo Router
  • NativeWind
  • expo-camera
  • EAS Build
  • OTA Updates
  • Push Notifications

1. Expo Setup — create-expo-app, app.json und Development Build

Expo ist das führende Framework für React Native Apps. Mit create-expo-app startet Claude Code ein neues Projekt in Sekunden — mit sofort funktionierender iOS- und Android-Unterstützung, ohne native Build-Tools manuell einzurichten.

Neues Projekt anlegen

# Neues Expo-Projekt mit TypeScript-Template
npx create-expo-app@latest MyApp --template blank-typescript
cd MyApp

# Abhängigkeiten installieren
npm install

# Expo Go starten (schnelles Prototyping)
npx expo start

Expo Go ermöglicht sofortiges Testen auf dem eigenen Gerät via QR-Code — ideal für die Entwicklung ohne Build-Schritt. Sobald native Module benötigt werden, wechselt Claude Code auf einen Development Build.

app.json — Projekt-Konfiguration

// app.json — Expo-Projekt-Konfiguration
{
  "expo": {
    "name": "MyApp",
    "slug": "myapp",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "scheme": "myapp",
    "userInterfaceStyle": "automatic",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#0a0a0f"
    },
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.mycompany.myapp",
      "buildNumber": "1"
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#0a0a0f"
      },
      "package": "com.mycompany.myapp",
      "versionCode": 1
    },
    "plugins": [
      "expo-router",
      "expo-notifications",
      ["expo-camera", { "cameraPermission": "Zugriff auf Kamera" }]
    ],
    "experiments": {
      "typedRoutes": true
    }
  }
}

app.config.ts statt app.json: Für dynamische Konfiguration — z. B. unterschiedliche Bundle-IDs per Umgebungsvariable — empfiehlt Claude Code app.config.ts, das JavaScript-Logik erlaubt und zur Build-Zeit ausgeführt wird.

Expo Go vs Development Build

ModusVorteileEinschränkungen
Expo Go Sofort einsatzbereit, kein Build nötig, QR-Code-Scan Keine custom native Modules, kein vollständiges API-Set
Development Build Alle native Module, vollständige Expo-API, Debug-Tools EAS Build oder lokale Xcode/Android Studio nötig
Production Build Optimiert, App Store ready, EAS Submit Kein Hot-Reload, Sign-Certificates erforderlich
# Development Build erstellen
npx expo install expo-dev-client
eas build --profile development --platform ios

# SDK-Version prüfen und upgraden
npx expo install --check
npx expo install --fix

2. Expo Router — File-based Navigation

Expo Router bringt das bewährte Next.js-Prinzip der dateibasierten Navigation zu React Native. Claude Code strukturiert die app/-Ordner so, dass Routen automatisch aus der Verzeichnisstruktur entstehen — kein manuelles Stack-Konfigurieren mehr.

Ordnerstruktur

app/
├── _layout.tsx          # Root Layout (Fonts, Provider)
├── index.tsx            # Home-Screen (/)
├── (tabs)/
│   ├── _layout.tsx      # Tab-Navigator
│   ├── index.tsx        # Tab: Home
│   ├── explore.tsx      # Tab: Explore
│   └── profile.tsx      # Tab: Profile
├── (auth)/
│   ├── _layout.tsx      # Auth-Stack
│   ├── login.tsx
│   └── register.tsx
├── product/
│   └── [id].tsx         # Dynamische Route: /product/123
└── +not-found.tsx       # 404-Screen

Root Layout — _layout.tsx

import { Stack } from 'expo-router';
import { useFonts } from 'expo-font';
import * as SplashScreen from 'expo-splash-screen';
import { useEffect } from 'react';

SplashScreen.preventAutoHideAsync();

export default function RootLayout() {
  const [loaded] = useFonts({
    SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
  });

  useEffect(() => {
    if (loaded) SplashScreen.hideAsync();
  }, [loaded]);

  if (!loaded) return null;

  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen name="(auth)" options={{ headerShown: false }} />
      <Stack.Screen name="+not-found" />
    </Stack>
  );
}

Tab Navigator

// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';

export default function TabLayout() {
  return (
    <Tabs screenOptions={{
      tabBarActiveTintColor: '#818cf8',
      tabBarStyle: { backgroundColor: '#0a0a0f', borderTopColor: '#1e1e3a' },
    }}>
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarIcon: ({ color }) =>
            <Ionicons name="home" size={24} color={color} />,
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: 'Profil',
          tabBarIcon: ({ color }) =>
            <Ionicons name="person" size={24} color={color} />,
        }}
      />
    </Tabs>
  );
}

Navigation mit Link, useRouter und useLocalSearchParams

import { Link, useRouter, useLocalSearchParams } from 'expo-router';
import { View, Text, Button } from 'react-native';

// Deklarative Navigation via Link-Komponente
function HomeScreen() {
  const router = useRouter();

  return (
    <View>
      {/* Deklarativ */}
      <Link href="/product/42">Produkt öffnen</Link>

      {/* Imperativ */}
      <Button
        title="Zum Profil"
        onPress={() => router.push('/profile')}
      />

      {/* Mit Parametern */}
      <Button
        title="Artikel öffnen"
        onPress={() => router.push({ pathname: '/product/[id]', params: { id: '99' } })}
      />
    </View>
  );
}

// Dynamische Route: app/product/[id].tsx
function ProductScreen() {
  const { id } = useLocalSearchParams<{ id: string }>();
  return <Text>Produkt ID: {id}</Text>;
}

3. NativeWind — Tailwind CSS für React Native

NativeWind bringt Tailwind CSS-Klassen zu React Native. Claude Code setzt className-Props auf native Komponenten ein — dieselben Utility-Klassen wie im Web, aber optimiert für iOS und Android.

Installation und Setup

# Installation
npm install nativewind tailwindcss
npx tailwindcss init

# tailwind.config.js
module.exports = {
  content: ['./app/**/*.{js,jsx,ts,tsx}', './components/**/*.{js,jsx,ts,tsx}'],
  presets: [require('nativewind/preset')],
  theme: {
    extend: {
      colors: {
        primary: '#818cf8',
        surface: '#0a0a0f',
      }
    }
  },
  plugins: [],
};
// babel.config.js
module.exports = {
  presets: ['babel-preset-expo'],
  plugins: ['nativewind/babel'],
};

Komponenten mit className

import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
import { useColorScheme } from 'nativewind';

export function ProductCard({ title, price }: { title: string; price: number }) {
  const { colorScheme } = useColorScheme();

  return (
    <View className="bg-white dark:bg-gray-900 rounded-2xl p-4 mx-4 mb-3 shadow-md">
      <Text className="text-xl font-bold text-gray-900 dark:text-white mb-1">
        {title}
      </Text>
      <Text className="text-indigo-500 font-semibold text-lg">
        €{price.toFixed(2)}
      </Text>
      <TouchableOpacity className="bg-indigo-500 active:bg-indigo-700 rounded-xl py-3 mt-3 items-center">
        <Text className="text-white font-semibold">In den Warenkorb</Text>
      </TouchableOpacity>
    </View>
  );
}

// Dark Mode automatisch mit useColorScheme
export function ThemedScreen() {
  const { colorScheme, toggleColorScheme } = useColorScheme();
  const isDark = colorScheme === 'dark';

  return (
    <ScrollView className="flex-1 bg-white dark:bg-gray-950">
      <Text className="text-gray-900 dark:text-white text-2xl font-bold p-6">
        {isDark ? 'Dark Mode aktiv' : 'Light Mode aktiv'}
      </Text>
    </ScrollView>
  );
}

Platform-specific Styles

import { Platform, View, Text } from 'react-native';

// Methode 1: Platform.select
const styles = {
  container: Platform.select({
    ios: 'pt-14 px-4',    // Safe Area iOS
    android: 'pt-6 px-4',  // Android StatusBar
    default: 'pt-4 px-4',
  }),
};

// Methode 2: Platform-Dateien
// Header.ios.tsx  → wird automatisch auf iOS geladen
// Header.android.tsx → wird automatisch auf Android geladen

// Methode 3: Inline className mit Platform-Check
function SafeContainer({ children }: { children: React.ReactNode }) {
  return (
    <View className={`flex-1 ${Platform.OS === 'ios' ? 'pt-14' : 'pt-6'}`}>
      {children}
    </View>
  );
}

Tipp von Claude Code: NativeWind v4 unterstützt Tailwind v3 vollständig. Für Custom-Fonts und Custom-Colors empfiehlt Claude Code die Erweiterung des Tailwind-Themes in tailwind.config.js statt inline-Styles.

4. Native Modules — Camera, Location, Notifications und mehr

Expo stellt eine umfangreiche Bibliothek vorgefertigter nativer Module bereit. Claude Code integriert sie via Config Plugins automatisch in den Build — ohne manuelle Änderungen an Xcode oder Android Studio.

expo-camera — Kamerazugriff

import { CameraView, useCameraPermissions } from 'expo-camera';
import { useState, useRef } from 'react';
import { View, Text, Button } from 'react-native';

export function CameraScreen() {
  const [permission, requestPermission] = useCameraPermissions();
  const [facing, setFacing] = useState<'front' | 'back'>('back');
  const cameraRef = useRef<CameraView>(null);

  if (!permission) return <View />;
  if (!permission.granted) {
    return (
      <View className="flex-1 items-center justify-center">
        <Text>Kamera-Zugriff erforderlich</Text>
        <Button title="Erlauben" onPress={requestPermission} />
      </View>
    );
  }

  async function takePicture() {
    const photo = await cameraRef.current?.takePictureAsync({ quality: 0.8 });
    console.log('Foto URI:', photo?.uri);
  }

  return (
    <CameraView ref={cameraRef} style={{ flex: 1 }} facing={facing}>
      <Button title="📷 Foto" onPress={takePicture} />
      <Button title="🔄 Wechseln" onPress={() => setFacing(f => f === 'back' ? 'front' : 'back')} />
    </CameraView>
  );
}

expo-location — GPS und Standort

import * as Location from 'expo-location';
import { useEffect, useState } from 'react';

export function useCurrentLocation() {
  const [location, setLocation] = useState<Location.LocationObject | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    (async () => {
      const { status } = await Location.requestForegroundPermissionsAsync();
      if (status !== 'granted') {
        setError('Standort-Zugriff verweigert');
        return;
      }
      const loc = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.High });
      setLocation(loc);
    })();
  }, []);

  return { location, error };
}

expo-secure-store und expo-file-system

import * as SecureStore from 'expo-secure-store';
import * as FileSystem from 'expo-file-system';

// Sicher speichern (Keychain/Keystore)
await SecureStore.setItemAsync('auth_token', token);
const token = await SecureStore.getItemAsync('auth_token');
await SecureStore.deleteItemAsync('auth_token');

// Datei-Download
const fileUri = FileSystem.documentDirectory + 'report.pdf';
const { uri } = await FileSystem.downloadAsync(
  'https://api.example.com/report.pdf',
  fileUri
);
console.log('Gespeichert unter:', uri);

// Datei-Infos
const info = await FileSystem.getInfoAsync(fileUri);
if (info.exists) console.log('Größe:', info.size, 'bytes');

Wichtig: Native Module wie expo-camera und expo-location funktionieren NICHT in Expo Go, wenn sie als Config Plugin in app.json konfiguriert sind. Claude Code wechselt automatisch auf Development Builds für solche Features.

5. EAS Build und Submit — App Store und Play Store

EAS (Expo Application Services) ist Explos Cloud-Build-Plattform. Claude Code richtet die komplette Build-Pipeline ein — von der eas.json-Konfiguration bis zum automatischen App Store Submit.

EAS Setup

# EAS CLI installieren
npm install -g eas-cli

# Bei Expo einloggen
eas login

# Projekt initialisieren
eas init

eas.json — Build-Profile

{
  "cli": { "version": ">= 5.0.0" },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "ios": { "simulator": true }
    },
    "preview": {
      "distribution": "internal",
      "ios": { "enterpriseProvisioning": "adhoc" }
    },
    "production": {
      "autoIncrement": true,
      "ios": { "resourceClass": "m-medium" },
      "android": { "buildType": "aab" }
    }
  },
  "submit": {
    "production": {
      "ios": {
        "appleId": "dev@example.com",
        "ascAppId": "1234567890",
        "appleTeamId": "ABC1234DEF"
      },
      "android": {
        "serviceAccountKeyPath": "./google-service-account.json",
        "track": "internal"
      }
    }
  }
}

Build-Befehle

# Development Build (Simulator)
eas build --profile development --platform ios

# Production Build für beide Plattformen
eas build --profile production --platform all

# Nur Android AAB (für Google Play)
eas build --profile production --platform android

# Nur iOS IPA (für App Store)
eas build --profile production --platform ios

# Build-Status prüfen
eas build:list

eas submit — App Store und Play Store

# iOS → App Store Connect (TestFlight)
eas submit --platform ios --latest

# Android → Google Play (Internal Track)
eas submit --platform android --latest

# Beide gleichzeitig
eas submit --platform all --latest

# Spezifischen Build submittieren
eas submit --platform ios --id BUILD_ID

⚙️ CI/CD mit EAS + GitHub Actions

Claude Code richtet automatisch eine GitHub Actions Pipeline ein, die bei jedem Push auf main einen EAS Production Build triggert und nach erfolgreichem Build direkt submitted.

  • eas build --non-interactive
  • eas submit --non-interactive
  • EXPO_TOKEN als Secret
  • Auto-Increment Version

6. OTA Updates und Push Notifications

Mit expo-updates lassen sich JavaScript-Änderungen ohne App Store Review direkt auf Nutzergeräte pushen. Kombiniert mit dem Expo Notification Service entsteht eine vollständige Post-Launch-Infrastruktur.

expo-updates — OTA Updates

# Installation
npx expo install expo-updates
import * as Updates from 'expo-updates';
import { useEffect, useState } from 'react';
import { Alert } from 'react-native';

export function useAppUpdates() {
  const [updateAvailable, setUpdateAvailable] = useState(false);

  useEffect(() => {
    async function checkForUpdates() {
      if (__DEV__) return; // Nur in Production

      try {
        const update = await Updates.checkForUpdateAsync();
        if (update.isAvailable) {
          setUpdateAvailable(true);
          await Updates.fetchUpdateAsync();
          Alert.alert(
            'Update verfügbar',
            'Ein neues Update wurde geladen. App neu starten?',
            [{ text: 'Neustart', onPress: () => Updates.reloadAsync() }]
          );
        }
      } catch (e) {
        console.warn('Update-Prüfung fehlgeschlagen:', e);
      }
    }
    checkForUpdates();
  }, []);

  return { updateAvailable };
}

eas update — Update deployen

# Update auf production-Channel deployen
eas update --channel production --message "Bugfix: Checkout-Fehler behoben"

# Update auf staging deployen
eas update --channel staging --message "Neues Feature: Dark Mode"

# Rollback — vorheriges Update reaktivieren
eas channel:edit production --rollout-percentage 50  # Canary Release

# Update-Verlauf anzeigen
eas update:list --branch production

Push Notifications mit expo-notifications

import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
import { Platform } from 'react-native';

// Notification-Handler konfigurieren
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
});

export async function registerForPushNotifications(): Promise<string | undefined> {
  if (!Device.isDevice) {
    console.warn('Push Notifications nur auf echten Geräten');
    return;
  }

  const { status: existingStatus } = await Notifications.getPermissionsAsync();
  let finalStatus = existingStatus;

  if (existingStatus !== 'granted') {
    const { status } = await Notifications.requestPermissionsAsync();
    finalStatus = status;
  }
  if (finalStatus !== 'granted') return;

  // Expo Push Token abrufen
  const projectId = Constants.expoConfig?.extra?.eas?.projectId;
  const token = (await Notifications.getExpoPushTokenAsync({ projectId })).data;

  // Android Notification-Kanal
  if (Platform.OS === 'android') {
    await Notifications.setNotificationChannelAsync('default', {
      name: 'Standard',
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
      lightColor: '#818cf8',
    });
  }

  return token;
}

// Push Notification senden (Server-Side via Expo Push API)
async function sendPushNotification(expoPushToken: string, title: string, body: string) {
  const message = {
    to: expoPushToken,
    sound: 'default',
    title,
    body,
    data: { screen: 'home' },
    badge: 1,
  };

  await fetch('https://exp.host/--/api/v2/push/send', {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Accept-Encoding': 'gzip, deflate',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(message),
  });
}

Lokale Notifications (ohne Server)

// Geplante Notification (z. B. Reminder)
await Notifications.scheduleNotificationAsync({
  content: {
    title: '🏋️ Workout-Zeit!',
    body: 'Dein tägliches Training wartet auf dich.',
    data: { type: 'workout-reminder' },
  },
  trigger: {
    hour: 8,
    minute: 30,
    repeats: true,
  },
});

// Sofortige lokale Notification
await Notifications.scheduleNotificationAsync({
  content: { title: 'Erledigt!', body: 'Dein Export ist fertig.' },
  trigger: null, // Sofort
});

// Alle Notifications entfernen
await Notifications.cancelAllScheduledNotificationsAsync();

🚀 Expo Notification Service vs. FCM/APNs direkt

Der Expo Push Notification Service leitet an FCM (Android) und APNs (iOS) weiter — ohne dass du eigene Zertifikate oder Firebase-Integration einrichten musst. Für Production-Apps mit hohem Volumen empfiehlt Claude Code den direkten FCM/APNs-Zugang via EAS.

Claude Code konfiguriert beides — einfacher Start mit Expo ENS, Migration zu direktem FCM/APNs bei Bedarf.

Notification-Listener in der App

import { useEffect, useRef } from 'react';
import * as Notifications from 'expo-notifications';
import { useRouter } from 'expo-router';

export function useNotificationNavigation() {
  const router = useRouter();
  const notificationListener = useRef<Notifications.EventSubscription>();
  const responseListener = useRef<Notifications.EventSubscription>();

  useEffect(() => {
    // Notification empfangen (App im Vordergrund)
    notificationListener.current = Notifications.addNotificationReceivedListener(n => {
      console.log('Notification erhalten:', n.request.content.title);
    });

    // Nutzer tippt auf Notification
    responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
      const data = response.notification.request.content.data;
      if (data.screen) router.push(`/${data.screen}`);
    });

    return () => {
      notificationListener.current?.remove();
      responseListener.current?.remove();
    };
  }, []);
}

Mit Claude Code Mobile Apps bauen

Claude Code richtet Expo-Projekte vollständig ein — von der ersten Zeile Code bis zum App Store Submit. Starte jetzt deinen kostenlosen Trial.

Kostenlosen Trial starten →