Skip to Content

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;DRfetch existe 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 + setTimeoutfetch(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.