Skip to Content
React Native6. État & formulaires6.1 Hooks en contexte RN

Chapitre 6.1 — Les hooks en contexte React Native

Où on en est : Drill navigue, mais tout est statique. Avant d’introduire un store, on recalibre vos réflexes hooks pour le mobile : ce qui change, c’est le COÛT des re-renders et le cycle de vie d’une app.

⏱️ TL;DRuseState/useEffect/useMemo : identiques. Ce qui change : (1) les re-renders coûtent plus cher (pont vers le natif) — la discipline useCallback/memo compte surtout autour des FlatList ; (2) le cycle de vie a une dimension nouvelle : l’app passe en arrière-plan (AppState) — crucial pour un timer de quiz ; (3) useFocusEffect remplace useEffect pour « à chaque visite de l’écran ».

🎯 Objectifs

  • Savoir OÙ la discipline anti-re-render est rentable en RN (et où elle ne l’est pas).
  • Gérer l’arrière-plan avec AppState — le cas du timer de Drill.
  • Utiliser useFocusEffect pour les écrans sous pile/tabs.
  • Poser les règles d’hygiène d’état du cours.

Vos hooks, votre React

Rien à réapprendre : useState, useEffect, useReducer, useMemo, useCallback, useRef, contexte — tout est le MÊME React (19.2). Une partie de Drill pourrait s’écrire entièrement en useState. Ce chapitre calibre les trois différences d’environnement.

Différence 1 : le coût des re-renders

Un re-render web ne touche le DOM que s’il y a diff. Même chose en RN (Fabric ne commit que le diff)… mais chaque mutation traverse JSI vers le natif, et le thread JS mobile est plus lent qu’un desktop. En pratique :

  • Écrans simples (réglages, résultat) : zéro discipline nécessaire. N’optimisez pas.
  • Listes : LE point chaud. Un re-render du parent d’une FlatList re-rend tous les items visibles si les props ne sont pas stables. D’où le triptyque déjà vu (chapitre 3.4) : React.memo(Item) + useCallback sur les handlers passés aux items + styles statiques.
  • Animations : jamais via re-renders JS (60-120 re-renders/s = mission impossible) — c’est le boulot de Reanimated (Partie 10), qui contourne React complètement.
// L'écran Thèmes — la discipline appliquée UNE fois, au bon endroit const handleOpen = useCallback((slug: string) => router.push(`/themes/${slug}`), []) <FlatList data={themes} renderItem={({ item }) => <ThemeRow theme={item} onOpen={handleOpen} />} /> // ThemeRow = React.memo(…) — et c'est tout. Pas de useCallback ailleurs dans l'écran.

💡 Pour un dev Next.js — Même philosophie que le React Compiler vous l’a apprise : la mémoïsation manuelle n’est pas une vertu, c’est un outil ciblé. En RN 2026, le compiler arrive aussi (activable dans Expo) mais la règle reste : profiler d’abord (chapitre 2.4), optimiser les listes, ignorer le reste.

Différence 2 : l’app a un arrière-plan

Le web a des onglets ; le mobile a un cycle de vie d’app : active → background (l’user répond à un SMS) → de retour. Aucun événement web ne correspond vraiment. L’API : AppState.

Le cas concret de Drill — le timer : le joueur a 20 s pour répondre ; il met l’app en arrière-plan à 12 s… le temps doit-il continuer ? (Oui — sinon pause tactique gratuite !) :

// hooks/useAppStateChange.ts import { useEffect, useRef } from 'react' import { AppState, AppStateStatus } from 'react-native' export function useAppStateChange(onChange: (status: AppStateStatus) => void) { const cb = useRef(onChange) cb.current = onChange useEffect(() => { const sub = AppState.addEventListener('change', status => cb.current(status)) return () => sub.remove() }, []) }

Le principe pour un timer honnête : ne JAMAIS compter en décrémentant un state chaque seconde (le JS est gelé en arrière-plan !) — stocker l’heure de fin (endsAt = Date.now() + 20_000) et calculer le restant à chaque tick ET au retour d’arrière-plan. On l’implémente au chapitre 6.3 ; retenez le pattern : l’horloge fait foi, pas le compteur.

⚠️ Piège — Un setInterval n’est PAS fiable en arrière-plan (throttlé/gelé selon l’état de l’app et d’Android). Tout ce qui dépend du temps réel — timer de jeu, expiration de cache, cooldown — se base sur des timestamps comparés à Date.now(), jamais sur l’accumulation de ticks.

Différence 3 : focus ≠ montage

Vu au chapitre 5.1 : les écrans restent montés sous la pile et dans les tabs. useEffect([]) = « au montage » = une fois par vie d’écran, pas à chaque visite. Pour rafraîchir à chaque visite :

import { useFocusEffect } from 'expo-router' import { useCallback } from 'react' useFocusEffect( useCallback(() => { reloadHistory() // à CHAQUE focus de l'écran return () => {} // au blur (départ de l'écran) }, [reloadHistory]) )

Cas d’usage dans Drill : l’Historique se rafraîchit quand on y revient après une partie (les données SQLite auront changé — Partie 8) ; l’écran de quiz coupe ses sons au blur.

L’hygiène d’état du cours

Trois règles qu’on suivra systématiquement :

  1. L’état le plus local possible : un toggle d’accordéon = useState dans le composant. N’escaladez vers un store que ce qui est PARTAGÉ entre écrans.
  2. Dériver plutôt que stocker : const isLast = index === total - 1 — pas un state. Un état stocké qui peut se calculer est un bug de synchronisation en attente.
  3. La logique de jeu dans lib/, pure : le calcul de score, le tirage de questions — des fonctions TS testables. Les hooks/stores les appellent.

✏️ Exercices

1. Écrivez lib/scoring.ts : computePoints(secondsLeft: number, total: number): number — 1000 points max, dégressifs linéairement avec le temps restant, minimum 100 si correct. Pure, testable.

✅ Solution

// lib/scoring.ts export function computePoints(secondsLeft: number, totalSeconds: number): number { if (totalSeconds <= 0) return 100 const ratio = Math.max(0, Math.min(1, secondsLeft / totalSeconds)) return Math.round(100 + ratio * 900) }

Réponse instantanée = 1000, dernière seconde = ~100. Bornée, sans dépendance — Jest la testera en Partie 14 sans aucun mock.

2. Le header de l’accueil doit afficher « Bon retour 👋 » si l’app revient d’un arrière-plan de plus de 5 minutes. Implémentez avec useAppStateChange.

✅ Solution

const wentBackgroundAt = useRef<number | null>(null) const [showWelcomeBack, setShowWelcomeBack] = useState(false) useAppStateChange(status => { if (status === 'background') wentBackgroundAt.current = Date.now() if (status === 'active' && wentBackgroundAt.current) { if (Date.now() - wentBackgroundAt.current > 5 * 60_000) setShowWelcomeBack(true) wentBackgroundAt.current = null } })

Timestamps comparés, pas de compteur — le pattern du chapitre appliqué.

3. Un collègue met useCallback sur TOUS les handlers de l’écran réglages « par principe ». Expliquez-lui, arguments concrets à l’appui, pourquoi c’est du bruit — et où le déplacer.

✅ Solution

L’écran réglages n’a ni liste ni enfants mémoïsés : les fonctions recréées à chaque render y sont gratuites (personne ne compare leur référence), et chaque useCallback ajoute lecture

  • tableau de deps à maintenir (source de bugs de staleness). La discipline paie UNIQUEMENT quand la référence est comparée : props d’un React.memo, deps d’un effet, items de FlatList. → Garder le réflexe pour l’écran Thèmes (liste), l’enlever ici.

🧠 Quiz

1. Où la mémoïsation est-elle rentable en RN ?

Réponse

Autour des FlatList (items memo + handlers useCallback + styles stables) et des props de composants mémoïsés. Ailleurs (écrans simples), c’est du bruit.

2. Pourquoi un timer par décrément de state est-il faux, et quel est le bon pattern ?

Réponse

Le JS est gelé/throttlé en arrière-plan : les ticks se perdent, le timer « offre » du temps. Bon pattern : stocker l’heure de fin et calculer endsAt - Date.now() à chaque tick et au retour d’arrière-plan — l’horloge fait foi.

3. Quelle API écoute les passages actif/arrière-plan ?

Réponse

AppState (AppState.addEventListener('change', cb)) — statuts active, background (et inactive sur iOS).

4. useEffect([]) vs useFocusEffect ?

Réponse

useEffect([]) = une fois au montage (l’écran reste monté sous pile/tabs). useFocusEffect = à chaque fois que l’écran redevient visible (+ cleanup au blur).

5. Citez les trois règles d’hygiène d’état du cours.

Réponse

État le plus local possible (n’escalader que le partagé) ; dériver plutôt que stocker ; logique de jeu pure dans lib/ (les hooks l’appellent).


👉 Chapitre suivant : 6.2 — Zustand — le store de session de Drill.