A Armadilha das Subscriptions em useEffect
Eu costumava escrever hooks de detecção de rede do jeito que a maioria dos vibe coders escreve: um useEffect que assina o NetInfo, atualiza estado quando a conectividade muda e torce para tudo dar certo. Funcionava em desenvolvimento. Falhava em produção.
Race conditions. Vazamentos de memória. Stale closures. Os suspeitos de sempre que não aparecem no preview do Cursor, mas aparecem nos seus crash logs.
É isso que o Claude Code gera quando você pede detecção de rede. Parece correto. Na prática, é um risco de produção:
// 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
}
Os problemas: tearing quando o callback dispara antes do effect rodar. Vazamentos de memória quando o unmount acontece durante o setup. Stale closures quando o callback referencia props e o dependency array está errado. Duplicação quando cada componente escreve o mesmo boilerplate.
Esses são os bugs que fazem os usuários deixarem reviews de uma estrela.
Entra em Cena o useSyncExternalStore
O React 18 introduziu useSyncExternalStore exatamente para isso. Ele foi feito para ler de stores externas: qualquer coisa com uma função subscribe e uma função getSnapshot. NetInfo é uma store externa. Seu estado de auth também. Qualquer módulo nativo que emite eventos também.
Em vez de escrever subscriptions frágeis com useEffect, deixe o React cuidar da parte difícil:
// 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
}
O adapter é dono de exatamente uma subscription nativa. Quando o NetInfo dispara, ele atualiza o snapshot interno e notifica todos os listeners. O hook conecta o React a essa 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
)
}
É só isso. O React cuida do lifecycle da subscription. O service cuida da subscription nativa. O hook só faz a ponte. Quando o componente monta, o React assina. Quando desmonta, o React cancela. Quando a store muda, o React renderiza de novo. Você não escreve nada dessa lógica. Você não consegue errar.
Um Padrão para Toda Store Externa
Isso escala lindamente. Auth, rede, permissões, lifecycle: qualquer estado vindo de fora do React pode usar o mesmo formato:
// 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
)
}
Antes do useSyncExternalStore, eu escrevia hooks customizados com useEffect para cada fonte externa de dados. Cada um era um pouco diferente. Cada um tinha seus próprios bugs. Agora eu escrevo um padrão, aplico em todo lugar e deixo o React cuidar do lifecycle da subscription.
Se você ainda está usando useEffect para subscriptions externas no seu app React Native feito com vibe coding, está trabalhando mais do que precisa e produzindo código mais bugado do que o necessário. O hook existe. Use.
O Autotomy Expo Starter Pack usa esse padrão para estado de rede, estado de auth e lifecycle do app. Um padrão, zero bugs de subscription, paz infinita.