Mobile & React Native

React Native mit Claude Code: Mobile Apps 2026

React Native mit Expo: Navigation, Zustand, Native APIs, Animationen, EAS Build — Claude Code entwickelt plattformübergreifende iOS- und Android-Apps mit TypeScript.

6. Mai 2026 12 min Lesezeit

React Native + Expo SDK 52 ist 2026 der Standard für produktionsreife mobile Apps — ein gemeinsamer TypeScript-Code für iOS und Android, native Performance und direkter Zugriff auf Kamera, GPS, Push-Benachrichtigungen und sichere Schlüsselverwaltung. Claude Code kennt die gesamte Expo-Ökosystem und generiert typsichere, getestete App-Architektur in wenigen Minuten.

React Native Expo SDK 52 Expo Router Reanimated 3 EAS Build TypeScript

Warum React Native?

Ein TypeScript-Codebase für iOS und Android. Native UI-Komponenten, keine WebView.

Warum Expo?

Expo SDK, EAS Build, OTA-Updates und file-basiertes Routing mit Expo Router out-of-the-box.

Claude Code Vorteil

Generiert Screens, Hooks, Native API-Integration und vollständige EAS-Konfiguration.

2026 Stack

Expo SDK 52, Expo Router v4, Reanimated 3, NativeWind v4, EAS Build + Submit.

1. Expo Grundlagen: Projekt-Setup und Architektur

Mit Expo startet ein neues React Native Projekt in Sekunden. Das create-expo-app-Template legt die komplette Ordnerstruktur, TypeScript-Konfiguration und das Metro Bundler-Setup automatisch an. Claude Code generiert zusätzlich eas.json, app.config.ts und den Dev-Client-Workflow.

Neues Expo-Projekt erstellen

# Neues Projekt mit Expo Router Template npx create-expo-app@latest MeineApp --template tabs # In Projektverzeichnis wechseln cd MeineApp # Expo Dev Client installieren (für native Modules) npx expo install expo-dev-client # Entwicklungsserver starten npx expo start --dev-client # EAS CLI global installieren npm install -g eas-cli eas login

app.config.ts — Typsichere App-Konfiguration

import { ExpoConfig, ConfigContext } from 'expo/config'; export default ({ config }: ConfigContext): ExpoConfig => ({ ...config, name: 'MeineApp', slug: 'meine-app', version: '1.0.0', orientation: 'portrait', icon: './assets/images/icon.png', scheme: 'meineapp', userInterfaceStyle: 'automatic', splash: { image: './assets/images/splash-icon.png', resizeMode: 'contain', backgroundColor: '#0f172a', }, ios: { supportsTablet: true, bundleIdentifier: 'com.company.meineapp', infoPlist: { NSCameraUsageDescription: 'Für Profilfotos und QR-Scans', NSLocationWhenInUseUsageDescription: 'Für standortbasierte Inhalte', }, }, android: { adaptiveIcon: { foregroundImage: './assets/images/adaptive-icon.png', backgroundColor: '#0f172a', }, package: 'com.company.meineapp', permissions: [ 'CAMERA', 'ACCESS_FINE_LOCATION', 'RECEIVE_BOOT_COMPLETED', ], }, plugins: [ 'expo-router', 'expo-secure-store', ['expo-camera', { cameraPermission: 'Kamerazugriff für QR-Code-Scanner' }], ], experiments: { typedRoutes: true, }, extra: { apiUrl: process.env.EXPO_PUBLIC_API_URL, eas: { projectId: process.env.EAS_PROJECT_ID }, }, });

app/_layout.tsx — Root Layout

import { Stack } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ThemeProvider } from '@react-navigation/native'; import { DarkTheme, DefaultTheme } from '@react-navigation/native'; import { useColorScheme } from 'react-native'; const queryClient = new QueryClient(); export default function RootLayout() { const colorScheme = useColorScheme(); return ( <QueryClientProvider client={queryClient}> <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> <StatusBar style="auto" /> <Stack> <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> <Stack.Screen name="(auth)" options={{ headerShown: false }} /> <Stack.Screen name="modal" options={{ presentation: 'modal' }} /> </Stack> </ThemeProvider> </QueryClientProvider> ); }

Metro Bundler: Konfiguration für Monorepos

2. Navigation: Expo Router und File-Based Routing

Expo Router v4 bringt file-based Routing direkt in React Native — analog zu Next.js App Router. Jede Datei im app/-Verzeichnis wird zu einer Route. Stack-Navigation, Tab-Navigation und Drawer-Navigation lassen sich einfach kombinieren. Mit typedRoutes in app.config.ts sind alle Routen vollständig typisiert.

Ordnerstruktur — File-Based Routing

app/ ├── _layout.tsx # Root Layout (QueryClient, Theme) ├── (auth)/ │ ├── _layout.tsx # Auth Stack Layout │ ├── login.tsx # Route: /login │ └── register.tsx # Route: /register ├── (tabs)/ │ ├── _layout.tsx # Tab Bar Layout │ ├── index.tsx # Route: / (Home Tab) │ ├── explore.tsx # Route: /explore │ └── profil.tsx # Route: /profil ├── produkt/ │ └── [id].tsx # Route: /produkt/:id (Dynamic) ├── [...missing].tsx # 404 Fallback └── modal.tsx # Route: /modal (Modal Presentation)

Tab-Navigation mit Expo Router

import { Tabs } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { useColorScheme } from 'react-native'; import Colors from '@/constants/Colors'; type IoniconName = keyof typeof Ionicons.glyphMap; function TabBarIcon({ name, color }: { name: IoniconName; color: string; }) { return <Ionicons name={name} size={24} color={color} />; } export default function TabsLayout() { const colorScheme = useColorScheme(); const tint = Colors[colorScheme ?? 'light'].tint; return ( <Tabs screenOptions={{ tabBarActiveTintColor: tint, headerShown: false, tabBarStyle: { backgroundColor: colorScheme === 'dark' ? '#1e293b' : '#fff', borderTopColor: colorScheme === 'dark' ? '#334155' : '#e2e8f0', }, }} > <Tabs.Screen name="index" options={{ title: 'Home', tabBarIcon: ({ color }) => <TabBarIcon name="home" color={color} />, }} /> <Tabs.Screen name="explore" options={{ title: 'Erkunden', tabBarIcon: ({ color }) => <TabBarIcon name="compass" color={color} />, }} /> <Tabs.Screen name="profil" options={{ title: 'Profil', tabBarIcon: ({ color }) => <TabBarIcon name="person" color={color} />, }} /> </Tabs> ); }

useRouter und useLocalSearchParams

// app/produkt/[id].tsx — Dynamische Route import { View, Text, Pressable, StyleSheet } from 'react-native'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { useQuery } from '@tanstack/react-query'; export default function ProduktScreen() { const { id } = useLocalSearchParams<{ id: string }>(); const router = useRouter(); const { data: produkt, isLoading } = useQuery({ queryKey: ['produkt', id], queryFn: () => fetchProdukt(id), enabled: !!id, }); if (isLoading) return <LoadingSpinner />; return ( <View style={styles.container}> <Text style={styles.title}>{produkt?.name}</Text> <Pressable style={styles.btn} onPress={() => router.push(`/warenkorb?add=${id}`)} > <Text style={styles.btnText}>In den Warenkorb</Text> </Pressable> <Pressable onPress={() => router.back()}> <Text>Zurück</Text> </Pressable> </View> ); }

Expo Router: Typsichere Routen mit TypeScript

3. Native APIs: Kamera, GPS, Notifications und SecureStore

Expo SDK 52 liefert über 50 native Module — von der Kamera bis zum biometrischen Sensor. Claude Code generiert den vollständigen Permission-Flow, Error-Handling und Plattform-spezifische Fallbacks für iOS und Android. Alle Module sind mit expo install versionssicher installierbar.

expo-camera: QR-Code-Scanner

import { useState, useEffect } from 'react'; import { View, Text, StyleSheet, Alert } from 'react-native'; import { CameraView, Camera, BarcodeScanningResult } from 'expo-camera'; export function QRScanner({ onScan }: { onScan: (data: string) => void }) { const [hasPermission, setHasPermission] = useState<boolean | null>(null); const [scanned, setScanned] = useState(false); useEffect(() => { (async () => { const { status } = await Camera.requestCameraPermissionsAsync(); setHasPermission(status === 'granted'); })(); }, []); const handleBarCodeScanned = ({ data }: BarcodeScanningResult) => { setScanned(true); onScan(data); Alert.alert('QR-Code gescannt', data, [ { text: 'Erneut scannen', onPress: () => setScanned(false) }, ]); }; if (hasPermission === null) return <Text>Kamerazugriff wird angefragt…</Text>; if (!hasPermission) return <Text>Kamerazugriff verweigert</Text>; return ( <View style={styles.container}> <CameraView style={StyleSheet.absoluteFillObject} facing="back" barcodeScannerSettings={{ barcodeTypes: ['qr', 'ean13'] }} onBarcodeScanned={scanned ? undefined : handleBarCodeScanned} /> <View style={styles.overlay}> <View style={styles.scanFrame} /> <Text style={styles.hint}>QR-Code im Rahmen positionieren</Text> </View> </View> ); }

expo-location: Standortbasierte Services

import * as Location from 'expo-location'; import { useEffect, useState } from 'react'; interface Koordinaten { latitude: number; longitude: number; accuracy: number | null; } export function useStandort() { const [koordinaten, setKoordinaten] = useState<Koordinaten | null>(null); const [fehler, setFehler] = useState<string | null>(null); const [laden, setLaden] = useState(true); useEffect(() => { let subscription: Location.LocationSubscription; (async () => { const { status } = await Location.requestForegroundPermissionsAsync(); if (status !== 'granted') { setFehler('Standortzugriff wurde verweigert'); setLaden(false); return; } subscription = await Location.watchPositionAsync( { accuracy: Location.Accuracy.High, timeInterval: 5000, distanceInterval: 10, }, (location) => { setKoordinaten({ latitude: location.coords.latitude, longitude: location.coords.longitude, accuracy: location.coords.accuracy, }); setLaden(false); } ); })(); return () => subscription?.remove(); }, []); return { koordinaten, fehler, laden }; }

expo-secure-store und expo-notifications

import * as SecureStore from 'expo-secure-store'; import * as Notifications from 'expo-notifications'; import { Platform } from 'react-native'; // SecureStore: Token sicher speichern (Keychain / Keystore) export const TokenManager = { async speichern(token: string): Promise<void> { await SecureStore.setItemAsync('auth_token', token); }, async laden(): Promise<string | null> { return SecureStore.getItemAsync('auth_token'); }, async loeschen(): Promise<void> { await SecureStore.deleteItemAsync('auth_token'); }, }; // Push-Benachrichtigungen registrieren export async function pushTokenAnfordern(): Promise<string | null> { if (Platform.OS === 'android') { await Notifications.setNotificationChannelAsync('default', { name: 'Standard', importance: Notifications.AndroidImportance.MAX, vibrationPattern: [0, 250, 250, 250], }); } const { status } = await Notifications.requestPermissionsAsync(); if (status !== 'granted') return null; const token = await Notifications.getExpoPushTokenAsync({ projectId: process.env.EXPO_PUBLIC_PROJECT_ID!, }); return token.data; }

Platform.OS — Plattformspezifische Logik

4. Styling & UI: StyleSheet, NativeWind und react-native-paper

React Native-Styling erfolgt über StyleSheet.create() — ein optimierter, plattformneutraler Ansatz der CSS-ähnliche Eigenschaften mit JavaScript-Objekten kombiniert. NativeWind v4 bringt Tailwind CSS-Klassen auf mobile, während react-native-paper Material Design 3 Komponenten liefert.

StyleSheet und Flexbox

import { View, Text, Pressable, StyleSheet, Platform } from 'react-native'; interface KarteProps { titel: string; beschreibung: string; onDruck: () => void; } export function ProduktKarte({ titel, beschreibung, onDruck }: KarteProps) { return ( <Pressable style={({ pressed }) => [styles.karte, pressed && styles.gedrueckt]} onPress={onDruck} > <View style={styles.kopf}> <Text style={styles.titel}>{titel}</Text> </View> <Text style={styles.text}>{beschreibung}</Text> </Pressable> ); } const styles = StyleSheet.create({ karte: { backgroundColor: '#1e293b', borderRadius: 12, padding: 16, marginVertical: 8, marginHorizontal: 16, // Plattformspezifischer Schatten ...Platform.select({ ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.15, shadowRadius: 8, }, android: { elevation: 4, }, }), }, gedrueckt: { opacity: 0.75, transform: [{ scale: 0.98 }], }, kopf: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8, }, titel: { fontSize: 16, fontWeight: '700', color: '#f1f5f9', flex: 1, }, text: { fontSize: 14, color: '#94a3b8', lineHeight: 20, }, });

NativeWind v4: Tailwind für React Native

// babel.config.js module.exports = { presets: ['babel-preset-expo'], plugins: ['nativewind/babel'], }; // Komponente mit NativeWind-Klassen import { View, Text, Pressable } from 'react-native'; import { styled } from 'nativewind'; const StyledView = styled(View); const StyledText = styled(Text); const StyledPressable = styled(Pressable); export function LoginFormular() { return ( <StyledView className="flex-1 bg-slate-900 items-center justify-center px-6"> <StyledText className="text-3xl font-bold text-white mb-8"> Anmelden </StyledText> <StyledView className="w-full bg-slate-800 rounded-xl p-4 mb-4"> <StyledText className="text-slate-400 text-sm mb-1">E-Mail</StyledText> <{/* TextInput hier */}> </StyledView> <StyledPressable className="w-full bg-indigo-500 rounded-xl py-4 items-center active:opacity-75" > <StyledText className="text-white font-bold text-base">Einloggen</StyledText> </StyledPressable> </StyledView> ); }
Styling-Ansatz Vorteil Nachteil Empfehlung
StyleSheet.create() Native Performance, typsicher Verbose, kein CSS-Nesting Basis für alle Apps
NativeWind v4 Tailwind-Workflow, Dark Mode Build-Overhead Utility-First Teams
react-native-paper Material 3, fertige Komponenten Bundle-Größe Enterprise / Material Design
Tamagui Universal (RN + Web), schnell Komplexes Setup Fullstack / Expo + Next.js

5. Animationen: react-native-reanimated 3 und Gesture Handler

react-native-reanimated 3 führt Animationen auf dem nativen UI-Thread aus — komplett unabhängig vom JavaScript-Thread. Damit sind butterweiche 60/120fps-Animationen auch bei rechenintensiven JS-Operationen möglich. Kombiniert mit react-native-gesture-handler entstehen intuitive Swipe-, Drag- und Pinch-Interaktionen.

useSharedValue und useAnimatedStyle

import Animated, { useSharedValue, useAnimatedStyle, withSpring, withTiming, withRepeat, withSequence, Easing, } from 'react-native-reanimated'; import { Pressable } from 'react-native'; export function AnimierterButton({ onPress, kinder }: { onPress: () => void; kinder: React.ReactNode }) { const scale = useSharedValue(1); const opacity = useSharedValue(1); const animierterStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }], opacity: opacity.value, })); const handlePressIn = () => { scale.value = withSpring(0.95, { damping: 15, stiffness: 300 }); opacity.value = withTiming(0.8, { duration: 100 }); }; const handlePressOut = () => { scale.value = withSpring(1, { damping: 10, stiffness: 200 }); opacity.value = withTiming(1, { duration: 150 }); }; return ( <Animated.View style={animierterStyle}> <Pressable onPress={onPress} onPressIn={handlePressIn} onPressOut={handlePressOut} > {kinder} </Pressable> </Animated.View> ); }

Gesture Handler: Swipeable Karten

import { GestureDetector, Gesture } from 'react-native-gesture-handler'; import Animated, { useSharedValue, useAnimatedStyle, withSpring, runOnJS, } from 'react-native-reanimated'; const SWIPE_THRESHOLD = 120; export function SwipeableKarte({ kinder, onSwipeLinks, onSwipeRechts, }: { kinder: React.ReactNode; onSwipeLinks: () => void; onSwipeRechts: () => void; }) { const offsetX = useSharedValue(0); const rotation = useSharedValue(0); const pan = Gesture.Pan() .onUpdate((e) => { offsetX.value = e.translationX; rotation.value = e.translationX * 0.05; }) .onEnd((e) => { if (e.translationX < -SWIPE_THRESHOLD) { runOnJS(onSwipeLinks)(); } else if (e.translationX > SWIPE_THRESHOLD) { runOnJS(onSwipeRechts)(); } else { // Zurück zur Mitte springen offsetX.value = withSpring(0, { damping: 12 }); rotation.value = withSpring(0, { damping: 12 }); } }); const animierterStyle = useAnimatedStyle(() => ({ transform: [ { translateX: offsetX.value }, { rotate: `${rotation.value}deg` }, ], })); return ( <GestureDetector gesture={pan}> <Animated.View style={animierterStyle}> {kinder} </Animated.View> </GestureDetector> ); }

Shared Element Transitions und Layout Animationen

import Animated, { FadeInDown, FadeOutUp, SlideInRight, Layout, ZoomIn, } from 'react-native-reanimated'; import { FlatList } from 'react-native'; function AnimierteListeItem({ item, index }: { item: Produkt; index: number }) { return ( <Animated.View entering={FadeInDown.delay(index * 80).springify()} exiting={FadeOutUp} layout={Layout.springify()} > <ProduktKarte {...item} /> </Animated.View> ); } // Lottie-Animation mit expo-av Alternative import LottieView from 'lottie-react-native'; export function LadeAnimation() { return ( <LottieView source={require('@/assets/animations/loading.json')} autoPlay loop style={{ width: 120, height: 120 }} /> ); }

Reanimated: Häufige Patterns

6. EAS Build & Deploy: App Store und Google Play

Expo Application Services (EAS) automatisiert den gesamten Build- und Deployment-Prozess. eas build kompiliert native Binaries in der Cloud, eas submit lädt direkt in App Store Connect und Google Play hoch. OTA-Updates über expo-updates ermöglichen sofortige JavaScript-Updates ohne App-Store-Review.

eas.json — Build-Profile

{ "cli": { "version": ">= 12.0.0", "appVersionSource": "remote" }, "build": { "development": { "developmentClient": true, "distribution": "internal", "ios": { "simulator": true }, "android": { "buildType": "apk" } }, "preview": { "distribution": "internal", "channel": "preview", "android": { "buildType": "apk" } }, "production": { "autoIncrement": true, "channel": "production", "ios": { "resourceClass": "m-medium" }, "android": { "buildType": "app-bundle" } } }, "submit": { "production": { "ios": { "appleId": "app@firma.de", "ascAppId": "1234567890", "appleTeamId": "ABCDE12345" }, "android": { "serviceAccountKeyPath": "./google-service-account.json", "track": "internal" } } } }

EAS Build Commands

# Alle Plattformen bauen (Cloud) eas build --platform all --profile production # Nur Android (schneller für Tests) eas build --platform android --profile preview # Lokaler iOS-Build (benötigt Mac + Xcode) eas build --platform ios --profile development --local # In App Stores einreichen eas submit --platform all --latest # Build-Status prüfen eas build:list --limit 5 # App Store Connect API Key einrichten eas credentials -p ios

OTA Updates mit expo-updates

import * as Updates from 'expo-updates'; import { useEffect } from 'react'; import { Alert } from 'react-native'; export function useUpdatePruefer() { useEffect(() => { async function pruefeAufUpdates() { if (__DEV__) return; // Im Dev-Mode überspringen try { const update = await Updates.checkForUpdateAsync(); if (update.isAvailable) { await Updates.fetchUpdateAsync(); Alert.alert( 'Update verfügbar', 'Eine neue Version wurde geladen. App jetzt neu starten?', [ { text: 'Später', style: 'cancel' }, { text: 'Jetzt neustarten', onPress: () => Updates.reloadAsync(), }, ] ); } } catch (e) { console.error('Update-Prüfung fehlgeschlagen:', e); } } pruefeAufUpdates(); }, []); } // OTA-Update pushen (ohne App-Store-Review) # eas update --channel production --message "Bugfix: Login-Flow"

CI/CD mit GitHub Actions + EAS

# .github/workflows/eas-build.yml name: EAS Build on: push: branches: [main] pull_request: branches: [main] jobs: build: name: EAS Build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm - name: Install dependencies run: npm ci - name: Setup EAS uses: expo/expo-github-action@v8 with: eas-version: latest token: ${{ secrets.EXPO_TOKEN }} - name: Build (Preview) run: eas build --platform android --profile preview --non-interactive - name: Update (OTA) if: github.ref == 'refs/heads/main' run: eas update --channel production --message "${{ github.event.head_commit.message }}" --non-interactive

App-Store-Readiness-Checkliste

Claude Code Workflow: React Native App von 0 auf Production

React Native Apps mit Claude Code entwickeln

Expo Router, Native APIs, Reanimated und EAS Build — Claude Code kennt den kompletten React Native Stack 2026 und generiert produktionsreife TypeScript-Apps für iOS und Android. Jetzt kostenlos starten und die erste Mobile-App entwickeln lassen.

14 Tage kostenlos testen →