React Native / Expo Muster
Praktische Muster für die Entwicklung produktionsreifer React Native Apps mit Expo. Behandelt Navigation, Zustand, Datenabruf, Listen, Styling und native APIs. Ergänzt das rules/react-native/ Regelwerk: Die Regeln sagen was durchgesetzt werden soll, dieses Skill zeigt wie.
Die unten genannten Bibliotheken (NativeWind, Zustand/Jotai, TanStack Query) sind gängige, etablierte Optionen, die zur Veranschaulichung gezeigt werden – die Muster sind wichtiger als das spezifische Paket, und jedes Äquivalent funktioniert. Zod wird für die Validierung verwendet, um Konsistenz mit den bestehenden typescript/-Regeln von ECC zu gewährleisten.
Diese Muster setzen den verwalteten Expo-Workflow (Expo Router, EAS, expo-* Module) auf der Neuen Architektur voraus (Standard in aktuellen Expo SDKs, ab SDK 55+ obligatorisch). Sie nehmen NICHT das Browser-DOM an – React Native hat kein <div>, keine URL-Leiste und keine Standard-Web-Datenabruf-Funktionen.
Wann aktivieren
Verwenden Sie dieses Skill, wenn:
- React Native / Expo Screens, Komponenten oder Navigation erstellt oder bearbeitet werden
- Routing mit Expo Router (dateibasiertes
app/-Verzeichnis) eingerichtet wird - Entscheidungen getroffen werden, wo Zustand hingehört (Server-Cache vs. Client-Store vs. Routenparameter vs. Formular)
- Datenabruf mit TanStack Query und Validierung der Antworten mit Zod verbunden wird
- Lange oder schwere Listen gerendert werden
- Ein Styling-Ansatz ausgewählt oder angewendet wird (NativeWind oder StyleSheet)
- Native Geräte-APIs (Kamera, Standort, Benachrichtigungen) oder sicherer Speicher verwendet werden
- RN-Code auf mobilspezifische Probleme überprüft wird
Verwenden Sie NICHT die Web/React-DOM-Muster hier – URL-als-Zustand, <div> und SWR-für-Browser gelten nicht für React Native.
Kernkonzepte
Projektstruktur (Expo Router)
Dateibasiertes Routing unter app/. Halten Sie Routendateien schlank: Sie lesen und validieren Parameter und delegieren dann an eine Screen-Komponente, die in components/ oder features/ lebt.
app/
_layout.tsx # Root Stack
(tabs)/
_layout.tsx # Tab-Navigator
index.tsx # Home
user/[id].tsx # dynamische Route
components/
features/
user/UserProfile.tsx
Navigation: Routenparameter validieren
Deep Links und dynamische Routen liefern nicht vertrauenswürdige Zeichenketten. Validieren Sie sie vor der Verwendung mit Zod.
// app/user/[id].tsx
import { useLocalSearchParams, router } from 'expo-router'
import { z } from 'zod'
import { UserProfile } from '@/features/user/UserProfile'
const Params = z.object({ id: z.string().uuid() })
export default function UserRoute() {
const parsed = Params.safeParse(useLocalSearchParams())
if (!parsed.success) {
router.replace('/not-found')
return null
}
return <UserProfile userId={parsed.data.id} />
}
Zustand: Belange getrennt halten
Duplizieren Sie keine Serverdaten in einen Client-Store. Jeder Belang hat sein eigenes Zuhause.
| Belang | Übliche Wahlmöglichkeiten |
|---|---|
| Serverzustand (entfernte Daten) | eine Server-Cache-Bibliothek (TanStack Query, SWR) |
| Client/UI-Zustand | ein leichter Store (Zustand, Jotai) oder Context |
| Routen-/Navigationszustand | Expo Router Parameter |
| Formularzustand | eine Formularbibliothek (z. B. React Hook Form) + Schema-Validierung |
| Geheimnisse / Tokens | expo-secure-store |
| Nicht-geheime Persistenz | AsyncStorage / MMKV |
Bevorzugen Sie lokales useState, bis Zustand wirklich geteilt werden muss.
Datenabruf: eine Cache-Bibliothek + Zod
Verwenden Sie eine Server-Cache-Bibliothek (TanStack Query, SWR) anstelle von fetch-in-useEffect. Validieren Sie an der Grenze und leiten Sie Typen aus dem Schema ab. Behandeln Sie Lade-, Fehler- und Leerzustände explizit. (Beispiel verwendet TanStack Query.)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { z } from 'zod'
const User = z.object({ id: z.string(), email: z.string().email() })
type User = z.infer<typeof User>
export function useUser(id: string) {
return useQuery({
queryKey: ['user', id],
queryFn: async (): Promise<User> => User.parse(await api.getUser(id)),
})
}
export function useUpdateEmail(id: string) {
const qc = useQueryClient()
return useMutation({
mutationFn: (email: string) => api.updateEmail(id, email),
onSuccess: () => qc.invalidateQueries({ queryKey: ['user', id] }),
})
}
Listen: virtualisieren, niemals ein großes Array in einer ScrollView mappen
import { FlatList } from 'react-native'
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={renderItem} // memoisiert
initialNumToRender={10}
windowSize={5}
/>
Verwenden Sie FlashList (Shopify) für große oder heterogene Listen.
Styling: ein System auswählen
StyleSheet.create() ist die framework-native Option; Utility-Class-Bibliotheken (z. B. NativeWind) sind eine gängige Alternative. Wählen Sie eines und bleiben Sie konsistent. Bauen Sie Style-Objekte niemals inline in JSX auf heißen Pfaden.
// NativeWind
<View className="p-4 rounded-2xl bg-white">
<Text className="text-base font-semibold">Hallo</Text>
</View>
// StyleSheet
const styles = StyleSheet.create({ card: { padding: 16, borderRadius: 16, backgroundColor: '#fff' } })
<View style={styles.card}>...</View>
Native APIs: in Hooks einkapseln, Effekte bereinigen
Halten Sie Expo SDK-Aufrufe und Abonnements in use*-Hooks, nicht in JSX. Bereinigen Sie immer.
import { useEffect, useState } from 'react'
import * as Location from 'expo-location'
type LocationState =
| { status: 'loading' }
| { status: 'denied' }
| { status: 'granted'; coords: Location.LocationObjectCoords }
export function useCurrentLocation() {
// Status verfolgen, nicht nur Koordinaten – damit die UI unterscheiden kann
// zwischen "wird noch geladen" und "Berechtigung verweigert" und eine umsetzbare Nachricht anzeigen kann.
const [state, setState] = useState<LocationState>({ status: 'loading' })
useEffect(() => {
let active = true
;(async () => {
const { status } = await Location.requestForegroundPermissionsAsync()
if (status !== 'granted') {
if (active) setState({ status: 'denied' })
return
}
const pos = await Location.getCurrentPositionAsync({})
if (active) setState({ status: 'granted', coords: pos.coords })
})()
return () => { active = false } // altes Ergebnis nach Unmount ignorieren
}, [])
return state
}
Sicheres Speichern für Tokens
import * as SecureStore from 'expo-secure-store'
await SecureStore.setItemAsync('auth_token', token) // Keychain / Keystore
const token = await SecureStore.getItemAsync('auth_token')
Codebeispiele
Ein vollständiger Screen: Route → Query → Liste → Zustände
// app/(tabs)/orders.tsx
import { memo, useCallback } from 'react'
import { FlatList, Text, View } from 'react-native'
import { useQuery } from '@tanstack/react-query'
import { z } from 'zod'
const OrderSchema = z.object({ id: z.string(), total: z.number(), status: z.string() })
const OrdersSchema = z.array(OrderSchema)
type Order = z.infer<typeof OrderSchema>
function useOrders() {
return useQuery({
queryKey: ['orders'],
queryFn: async () => OrdersSchema.parse(await api.listOrders()),
})
}
// Memoisiert, sodass die Referenz über Renderings hinweg stabil bleibt (siehe Listen-Richtlinien).
const OrderRow = memo(function OrderRow({ item }: { item: Order }) {
return (
<View className="px-4 py-3 border-b border-neutral-200">
<Text className="font-medium">#{item.id}</Text>
<Text className="text-neutral-500">{item.status} · ${item.total}</Text>
</View>
)
})
export default function OrdersScreen() {
const { data, isLoading, isError, refetch, isRefetching } = useOrders()
const renderItem = useCallback(({ item }: { item: Order }) => <OrderRow item={item} />, [])
if (isLoading) return <Centered><Text>Laden…</Text></Centered>
if (isError) return <Centered><Text accessibilityRole="alert">Bestellungen konnten nicht geladen werden.</Text></Centered>
if (!data?.length) return <Centered><Text>Noch keine Bestellungen.</Text></Centered>
return (
<FlatList
data={data}
keyExtractor={(o) => o.id}
onRefresh={refetch}
refreshing={isRefetching}
renderItem={renderItem}
/>
)
}
Ein Formular: React Hook Form + Zod Resolver
import { useForm, Controller } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { TextInput, Button, Text } from 'react-native'
const Schema = z.object({ email: z.string().email('Ungültige E-Mail') })
type FormValues = z.infer<typeof Schema>
export function EmailForm({ onSubmit }: { onSubmit: (v: FormValues) => void }) {
const { control, handleSubmit, formState: { errors } } = useForm<FormValues>({
resolver: zodResolver(Schema),
defaultValues: { email: '' },
})
return (
<>
<Controller
control={control}
name="email"
render={({ field: { value, onChange, onBlur } }) => (
<TextInput
value={value}
onChangeText={onChange}
onBlur={onBlur}
autoCapitalize="none"
keyboardType="email-address"
accessibilityLabel="E-Mail-Adresse"
/>
)}
/>
{errors.email && <Text accessibilityRole="alert">{errors.email.message}</Text>}
<Button title="Speichern" onPress={handleSubmit(onSubmit)} />
</>
)
}
Anti-Patterns
// FALSCH: großes Array in einer ScrollView gemappt (keine Virtualisierung, ruckelig, hoher Speicher)
<ScrollView>{items.map((i) => <Row key={i.id} item={i} />)}</ScrollView>
// RICHTIG: FlatList / FlashList
// FALSCH: Serverdaten in einen Client-Store kopiert (zwei Quellen der Wahrheit, veraltete Daten)
const useStore = create((set) => ({ users: [], setUsers: (u) => set({ users: u }) }))
useEffect(() => { getUsers().then(setUsers) }, [])
// RICHTIG: useQuery besitzt den Serverzustand; leiten Sie ab, was Sie benötigen
// FALSCH: Tokens in AsyncStorage (nicht verschlüsselt)
await AsyncStorage.setItem('auth_token', token)
// RICHTIG: expo-secure-store
// FALSCH: Deep-Link-Parametern vertrauen
const { id } = useLocalSearchParams(); fetchUser(id)
// RICHTIG: vor der Verwendung mit Zod validieren
// FALSCH: Inline-Style-Objekt, das bei jedem Rendern auf einem heißen Pfad neu erstellt wird
<View style={{ padding: 16, backgroundColor: '#fff' }} />
// RICHTIG: StyleSheet.create im Modulbereich oder NativeWind className
// FALSCH: Echtes Geheimnis im Bundle ausgeliefert
const STRIPE_SECRET = 'sk_live_...'
// RICHTIG: Privilegierte Aufrufe serverseitig lassen; nur öffentliche Schlüssel ausliefern, die durch Backend-Regeln geschützt sind
Best Practices
- Halten Sie Routendateien schlank; legen Sie Logik in Screen-Komponenten und
use*-Hooks. - Validieren Sie jede externe Eingabe (API-Antworten, Routenparameter, Push-Payloads) mit Zod.
- Lassen Sie TanStack Query den Serverzustand verwalten; halten Sie Client-Stores klein.
- Rendern Sie immer Lade-, Fehler- und Leerzustände – niemals nur einen Spinner ohne Fallback.
- Virtualisieren Sie Listen; memoieren Sie
renderItem; stellen Sie einen stabilenkeyExtractorbereit. - Verwenden Sie
react-native-reanimatedfür Animationen (UI-Thread); vermeiden Sie schwere Arbeit auf dem JS-Thread. - Speichern Sie Tokens in
expo-secure-store; vertrauen Sie dem Client niemals für die Autorisierung. - Respektieren Sie Safe Areas, Dynamic Type und Barrierefreiheitsrollen/-Labels von Anfang an.
- Bestätigen Sie die Kompatibilität mit der Neuen Architektur für jede native Abhängigkeit vor der Veröffentlichung.
Verwandte Skills
frontend-patterns– React/Next.js (Web)-Muster; nützlich für gemeinsame React-Konzepte, aber DOM-spezifisch.coding-standards– TypeScript/JavaScript-Idiome, die auf RN-Code anwendbar sind.tdd-workflow,e2e-testing– Testprozess (Jest + React Native Testing Library, Maestro/Detox für RN).security-review– Allgemeine Sicherheitscheckliste, die die RN-Bundle-/Geheimnis-Richtlinien oben ergänzt.