Die useEffect-Subscription-Falle
Ich habe Hooks zur Netzwerkerkennung früher so geschrieben, wie es die meisten Vibe Coder tun: ein useEffect, das NetInfo abonniert, den State aktualisiert, wenn sich die Konnektivität ändert, und dann hofft, dass schon alles gut geht. Im Development funktionierte das. In Produktion nicht.
Race Conditions. Memory Leaks. Stale Closures. Die üblichen Verdächtigen, die in deiner Cursor-Vorschau nicht auftauchen, dafür aber in deinen Crash Logs.
So sieht der Code aus, den Claude Code generiert, wenn du nach Netzwerkerkennung fragst. Er wirkt korrekt. In Produktion ist er eine Haftung:
// What Cursor gives you. Works in dev. Breaks in prod.
function useNetwork() {
const [isConnected, setIsConnected] = useState(true)
useEffect(() => {
const unsubscribe = NetInfo.addEventListener((state) => {
setIsConnected(!!state.isConnected)
})
return unsubscribe
}, [])
return isConnected
}
Die Probleme: Tearing, wenn der Callback feuert, bevor der Effect läuft. Memory Leaks, wenn ein Unmount während des Setups passiert. Stale Closures, wenn der Callback Props referenziert und das Dependency Array falsch ist. Duplikation, wenn jede Komponente dieselbe Boilerplate schreibt.
Das sind die Bugs, wegen denen Nutzer Ein-Stern-Bewertungen hinterlassen.
Jetzt kommt useSyncExternalStore
React 18 hat useSyncExternalStore genau für diesen Fall eingeführt. Der Hook ist dafür gemacht, aus externen Stores zu lesen - also aus allem, was eine subscribe-Funktion und eine getSnapshot-Funktion hat. NetInfo ist ein externer Store. Dein Auth State auch. Jedes native Modul, das Events emittiert, ebenfalls.
Anstatt fragiler useEffect-Subscriptions lässt du React die schwierigen Teile übernehmen:
// src/services/network/network.service.ts
// The service owns ONE native subscription.
import NetInfo from '@react-native-community/netinfo'
export type NetworkSnapshot = {
isConnected: boolean
isInternetReachable: boolean | null
connectionType: 'wifi' | 'cellular' | 'none' | 'unknown'
}
export interface NetworkService {
getSnapshot(): NetworkSnapshot
subscribe(listener: () => void): () => void
}
Der Adapter besitzt genau eine native Subscription. Wenn NetInfo feuert, aktualisiert er seinen internen Snapshot und benachrichtigt alle Listener. Der Hook verbindet React mit diesem Store:
// src/hooks/use-network.ts
// Three lines. No useEffect. No cleanup. No stale closures.
import { useSyncExternalStore } from 'react'
export function useNetwork() {
const { network } = useServices()
return useSyncExternalStore(
network.subscribe, // React handles subscribe/unsubscribe
network.getSnapshot, // Read current value
network.getSnapshot // SSR snapshot
)
}
Das ist alles. React besitzt den Subscription Lifecycle. Der Service besitzt die native Subscription. Der Hook baut nur die Brücke. Wenn die Komponente mountet, subscribed React. Wenn sie unmountet, unsubscribed React. Wenn sich der Store ändert, rendert React neu. Du schreibst keine dieser Logiken selbst. Du kannst sie nicht falsch machen.
Ein Pattern für jeden externen Store
Das skaliert hervorragend. Auth, Netzwerk, Berechtigungen, Lifecycle - jeder State von außerhalb von React kann dieselbe Form verwenden:
// Every external store follows the same contract:
interface ExternalStore<T> {
getSnapshot(): T
subscribe(listener: () => void): () => void
}
// Every hook looks the same:
function useStore<T>(store: ExternalStore<T>): T {
return useSyncExternalStore(
store.subscribe,
store.getSnapshot
)
}
Vor useSyncExternalStore habe ich für jede externe Datenquelle eigene useEffect-Hooks geschrieben. Jeder war ein bisschen anders. Jeder hatte seine eigenen Bugs. Jetzt schreibe ich ein Pattern, nutze es überall und lasse React den Subscription Lifecycle handhaben.
Wenn du in deiner vibe-codierten React-Native-App noch useEffect für externe Subscriptions schreibst, arbeitest du härter als nötig und produzierst fehleranfälligeren Code als notwendig. Der Hook existiert. Nutz ihn.
Das Autotomy Expo Starter Pack verwendet dieses Pattern für Network State, Auth State und App Lifecycle. Ein Pattern, null Subscription-Bugs, unendlich viel mehr Ruhe.