Chapitre 7.1 — fetch en mobile
Où on en est : Drill joue avec 5 questions en dur. Avant de brancher GitHub, un tour d’horizon de ce que « faire du réseau » signifie dans une app native — spoiler : ni CORS, ni cookies gratuits, mais des tunnels et du mode avion.
⏱️ TL;DR —
fetchexiste en RN, API identique au web. Différences : pas de CORS (aucun navigateur → aucune same-origin policy), pas de cookies automatiques fiables, et un réseau intrinsèquement instable qu’il faut respecter : timeout explicite (AbortController), gestion d’erreur systématique, et NetInfo pour connaître l’état de connexion. HTTPS obligatoire par défaut (cleartext bloqué par Android).
🎯 Objectifs
- Utiliser fetch en RN en connaissant les 3 différences avec le navigateur.
- Écrire un fetch robuste : timeout, erreurs typées, JSON vérifié.
- Détecter l’état du réseau avec @react-native-community/netinfo.
- Poser le client HTTP de Drill (lib/http.ts) qu’utilisera TanStack Query.
Le même fetch… sans navigateur autour
const res = await fetch('https://raw.githubusercontent.com/alex/drill-questions/main/manifest.json')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()Familier. Ce qui change autour :
1. Pas de CORS. La same-origin policy est une protection du navigateur : votre app
native n’en a pas. N’importe quelle API publique est appelable directement — pas de préflight,
pas de proxy, pas de header Access-Control-*. (Ça vous change des Route Handlers de
contournement !) Le revers : votre app peut appeler n’importe quoi… et n’importe qui peut voir
son trafic si vous laissiez passer du HTTP — d’où :
2. HTTPS obligatoire. Android bloque le cleartext (HTTP nu) par défaut. GitHub est en HTTPS, tout va bien ; retenez juste que « ça marche en dev avec http://192.168.x.x » (Metro a une exception) « mais plus en prod » est un classique de piège.
3. Pas de cookies automatiques fiables. Le stockage/renvoi de cookies existe mais n’est pas le modèle mental : les apps s’authentifient par tokens en header (hors sujet pour Drill — ADR 0001 : aucune auth).
Le réseau mobile est un milieu hostile
Chiffres à intérioriser : un utilisateur mobile passe par des tunnels, des ascenseurs, des WiFi captifs d’hôtel, le mode avion. Trois conséquences de code :
Timeout explicite. fetch peut attendre TRÈS longtemps par défaut. Toujours borner :
// lib/http.ts — le client de Drill
export class HttpError extends Error {
constructor(public status: number, message: string) { super(message) }
}
export async function fetchJson<T>(url: string, timeoutMs = 10_000): Promise<T> {
const controller = new AbortController()
const id = setTimeout(() => controller.abort(), timeoutMs)
try {
const res = await fetch(url, {
signal: controller.signal,
headers: { Accept: 'application/json' },
})
if (!res.ok) throw new HttpError(res.status, `HTTP ${res.status} on ${url}`)
return (await res.json()) as T
} finally {
clearTimeout(id)
}
}AbortController + setTimeout : le pattern standard (RN n’a pas d’option timeout native).
L’appelant recevra soit les données, soit une erreur rapide — jamais un spinner de 2 min.
Erreurs = flux normal. En mobile, l’échec réseau n’est pas exceptionnel, c’est un état de l’UI (chapitre 7.4 lui donnera ses écrans). Le try/catch n’est pas optionnel.
Connaître l’état du réseau. Pour adapter l’UI (bandeau « hors ligne », désactiver le pull-to-refresh) :
npx expo install @react-native-community/netinfo// hooks/useIsOnline.ts
import { useEffect, useState } from 'react'
import NetInfo from '@react-native-community/netinfo'
export function useIsOnline(): boolean {
const [online, setOnline] = useState(true)
useEffect(() => {
const unsub = NetInfo.addEventListener(state => {
setOnline(!!state.isConnected && state.isInternetReachable !== false)
})
return unsub
}, [])
return online
}Nuance importante : isConnected = « je vois un réseau » (WiFi captif inclus !) ;
isInternetReachable = « internet répond vraiment ». Le combo ci-dessus évite le piège du
portail d’hôtel.
⚠️ Piège — Ne bloquez jamais une action sur
useIsOnline(« pas de réseau → bouton désactivé ») : NetInfo peut se tromper (retard d’événement, réseau qui revient). Le réseau se découvre en essayant — NetInfo sert à informer l’UI et à déclencher des retries, pas à interdire. La seule vérité est la réponse (ou l’erreur) du fetch.
💡 Pour un dev Next.js — Votre plus gros désapprentissage ici : il n’y a pas de couche serveur à vous. Pas de Route Handler pour cacher une clé d’API, transformer une réponse, ou servir du cache : tout appel part DU téléphone, visible dans son trafic. Pour Drill, zéro problème (données publiques, lecture seule). Pour vos futures apps : toute clé secrète dans le code d’une app est publiée (l’APK se décompile) — les patterns à secrets exigent un backend, même minuscule. Drill n’en a pas besoin, par design.
✏️ Exercices
1. Testez fetchJson contre trois cibles : une URL valide (le manifest d’un repo public
GitHub à vous ou raw.githubusercontent.com/expo/expo/main/package.json), une 404, et un domaine
inexistant. Loggez le type d’erreur reçu dans chaque cas.
✅ Solution
Valide → données. 404 → HttpError avec status 404 (la branche !res.ok). Domaine inexistant →
TypeError: Network request failed (le fetch lui-même rejette — DNS). Trois familles à
distinguer dans l’UI : erreur serveur (4xx/5xx), erreur réseau (pas de connexion), timeout
(AbortError). Le chapitre 7.4 les mappe sur des états.
2. Ajoutez à fetchJson un retry simple : 2 tentatives supplémentaires, uniquement sur
erreur réseau/timeout (jamais sur 4xx), avec 1 s entre chaque.
✅ Solution
export async function fetchJsonRetry<T>(url: string, retries = 2): Promise<T> {
for (let attempt = 0; ; attempt++) {
try {
return await fetchJson<T>(url)
} catch (e) {
const retriable = !(e instanceof HttpError) || e.status >= 500
if (!retriable || attempt >= retries) throw e
await new Promise(r => setTimeout(r, 1000))
}
}
}4xx = votre requête est fausse, réessayer est inutile ; réseau/5xx = transitoire, réessayer a du sens. (TanStack Query fera ça mieux au chapitre suivant — comprendre le mécanisme d’abord.)
3. Affichez un bandeau « Hors ligne — les questions en cache restent jouables » en haut des
tabs quand useIsOnline() est false.
✅ Solution
Dans le layout des tabs : {!online && <View className="bg-amber-500/20 px-4 py-1.5"> <AppText variant="caption" className="text-amber-400">Hors ligne — questions en cache jouables</AppText></View>}. Informer sans bloquer (le piège du chapitre) — et le message
annonce déjà la couleur du chapitre 7.4 : offline ≠ cassé.
🧠 Quiz
1. Pourquoi n’y a-t-il pas de CORS en React Native ?
Réponse
CORS est une politique du navigateur (same-origin). Une app native n’a pas de navigateur ni d’origine — les requêtes partent librement, comme depuis un serveur ou curl.
2. Comment borne-t-on la durée d’un fetch en RN ?
Réponse
AbortController + setTimeout → fetch(url, { signal }), abort au timeout. Pas d’option
native — le pattern est à écrire (une fois, dans lib/http.ts).
3. Différence entre isConnected et isInternetReachable ?
Réponse
isConnected = attaché à un réseau (y compris WiFi captif sans internet).
isInternetReachable = internet répond réellement. Les deux ensemble pour un « online » honnête.
4. Pourquoi ne jamais désactiver un bouton sur la foi de NetInfo ?
Réponse
NetInfo peut retarder ou se tromper ; la seule vérité est la tentative elle-même. NetInfo informe (bandeau) et déclenche (refetch au retour du réseau) — il n’interdit pas.
5. Pourquoi une clé d’API secrète n’a-t-elle rien à faire dans le code d’une app ?
Réponse
L’app est distribuée : l’APK se décompile, le trafic s’observe — toute clé embarquée est publique. Les secrets vivent derrière un backend. Drill évite le sujet : données publiques en lecture seule (ADR 0001).
👉 Chapitre suivant : 7.2 — TanStack Query — le cache qui transforme fetch en expérience.