React Native / Expo Patterns (Muster für React Native / Expo)
Praktische Muster für die Entwicklung produktionsreifer React Native-Apps mit Expo. Behandelt Navigation, Zustand, Datenabruf, Listen, Styling und native APIs. Arbeitet mit dem rules/react-native/-Regelset zusammen: Regeln legen fest, was zu erzwingen ist – dieser 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 zur Validierung verwendet, um mit den bestehenden typescript/-Regeln von ECC konsistent zu bleiben.
Diese Muster setzen den verwalteten Expo-Workflow (Expo Router, EAS, expo-*-Module) auf der New Architecture voraus (der Standard in aktuellen Expo SDKs, ab SDK 55+ Pflicht). Sie setzen NICHT das Browser-DOM voraus – React Native hat kein <div>, keine URL-Leiste und keine Standard-Datenabruf-Methoden des Webs.
Wann aktivieren
Verwende diesen Skill bei:
- Erstellen oder Bearbeiten von React Native / Expo Screens, Komponenten oder Navigation
- Einrichten des Routings mit Expo Router (dateibasiertes
app/-Verzeichnis) - Entscheiden, wo der Zustand hingehört (Server-Cache vs. Client-Store vs. Routen-Parameter vs. Formular)
- Aufsetzen des Datenabrufs mit TanStack Query und Validieren der Antworten mit Zod
- Rendern langer oder schwerer Listen
- Auswählen oder Anwenden eines Styling-Ansatzes (NativeWind oder StyleSheet)
- Zugreifen auf native Geräte-APIs (Kamera, Standort, Benachrichtigungen) oder sicheren Speicher
- Überprüfen von RN-Code auf mobile-spezifische Probleme
Verwende NICHT die Web/React-DOM-Muster hier – URL-als-Zustand, <div> und SWR-für-Browser sind auf React Native nicht anwendbar.
Kernkonzepte
Projektstruktur (Expo Router)
Dateibasiertes Routing unter app/. Halte Routen-Dateien 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 # Startseite
user/[id].tsx # dynamische Route
components/
features/
user/UserProfile.tsx
Navigation: Routen-Parameter validieren
Deep Links und dynamische Routen liefern ungeprüfte Zeichenketten. Validiere 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
Dupliziere keine Serverdaten in einen Client-Store. Jeder Belang hat sein eigenes Zuhause.
| Belang | Übliche Wahlmöglichkeiten |
|---|---|
| Server-Zustand (Remote-Daten) | eine Server-Cache-Bibliothek (TanStack Query, SWR) |
| Client/UI-Zustand | ein leichtgewichtiger 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 |
Bevorzuge lokales useState, bis ein Zustand wirklich geteilt werden muss.
Datenabruf: eine Cache-Bibliothek + Zod
Verwende eine Server-Cache-Bibliothek (TanStack Query, SWR) anstelle von fetch in useEffect. Validiere an der Grenze und leite Typen aus dem Schema ab. Behandle 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 abbilden
import { FlatList } from 'react-native'
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={renderItem} // memoisiert
initialNumToRender={10}
windowSize={5}
/>
Verwende 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 übliche Alternative. Wähle eins und bleibe konsistent. Erstelle niemals Style-Objekte 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 kapseln, Effekte bereinigen
Halte Expo SDK-Aufrufe und Abonnements innerhalb von use*-Hooks, nicht in JSX. Bereinige 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() {
// Verfolge den Status, nicht nur die Koordinaten – damit die UI
// „wird noch geladen“ von „Berechtigung verweigert“ unterscheiden und
// eine hilfreiche 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 } // veraltetes Ergebnis nach Unmount ignorieren
}, [])
return state
}
Sicherer Speicher 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 → Abfrage → 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, damit die Referenz über Rendervorgänge hinweg stabil bleibt (siehe Listen-Hinweise).
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>Lädt…</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 (Anti-Muster)
// FALSCH: Großes Array innerhalb einer ScrollView abgebildet (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 Wahrheitsquellen, veraltete Daten)
const useStore = create((set) => ({ users: [], setUsers: (u) => set({ users: u }) }))
useEffect(() => { getUsers().then(setUsers) }, [])
// RICHTIG: useQuery besitzt den Server-Zustand; leite ab, was du benötigst
// 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 Verwendung mit Zod validieren
// FALSCH: Inline-Style-Objekt wird bei jedem Rendern auf einem heißen Pfad neu erstellt
<View style={{ padding: 16, backgroundColor: '#fff' }} />
// RICHTIG: StyleSheet.create auf Modulebene oder NativeWind className
// FALSCH: Echtes Geheimnis im Bundle ausgeliefert
const STRIPE_SECRET = 'sk_live_...'
// RICHTIG: Privilegierte Aufrufe serverseitig halten; nur öffentliche Schlüssel ausliefern, die durch Backend-Regeln geschützt sind
Best Practices (Bewährte Vorgehensweisen)
- Halte Routen-Dateien schlank; lege Logik in Screen-Komponenten und
use*-Hooks. - Validiere jede externe Eingabe (API-Antworten, Routen-Parameter, Push-Payloads) mit Zod.
- Lasse TanStack Query den Server-Zustand besitzen; halte Client-Stores klein.
- Rendere immer Lade-, Fehler- und Leerzustände – niemals nur einen Spinner ohne Fallback.
- Virtualisiere Listen; memoisiere
renderItem; sorge für einen stabilenkeyExtractor. - Verwende
react-native-reanimatedfür Animationen (UI-Thread); vermeide schwere Arbeit auf dem JS-Thread. - Speichere Tokens in
expo-secure-store; vertraue dem Client niemals für die Autorisierung. - Respektiere von Anfang an Safe Areas, Dynamic Type und Accessibilitäts-Rollen/-Labels.
- Bestätige vor der Veröffentlichung die Kompatibilität mit der New Architecture für jede native Abhängigkeit.
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 für RN-Code gelten.tdd-workflow,e2e-testing– Testprozess (verwende Jest + React Native Testing Library, Maestro/Detox für RN).security-review– Allgemeine Sicherheits-Checkliste, die die RN-Bundle-/Geheimnis-Hinweise oben ergänzt.