Module 6 · Le stack mobile

React Native : les briques natives

Tu connais déjà React (« Le modèle mental de React », « useState & useEffect »). Bonne nouvelle : React Native, c'est exactement le même cerveau — composants, props, JSX, hooks — mais au lieu de dessiner du HTML dans un navigateur, ton code pilote de vraies vues natives iOS et Android. Ce module t'apprend à reconnaître les nouvelles briques (qui remplacent div, span, button) pour que le code de Halterofit cesse de te sembler étranger.

💡 L'idée en une phrase

Tu écris du TypeScript, ça contrôle l'interface native. React Native n'est pas un « navigateur déguisé » : quand tu écris <View>, le système crée un vrai conteneur natif iOS/Android ; quand tu écris <Text>, c'est un vrai composant de texte natif. Ton fichier .tsx est la télécommande ; l'écran que l'utilisateur touche est, lui, 100 % natif. C'est pour ça que l'app a « l'air d'une vraie app », pas d'un site web dans une coquille.

1. Ce qu'est React Native (et ce qu'il n'est pas)

Quand tu fais du React « web », ton code finit par produire du HTML (des <div>, des <p>) que le navigateur affiche en manipulant le DOM — l'arbre des éléments de la page. React, là, est un chef d'orchestre : il décide quoi afficher, et un « moteur de rendu » (ReactDOM) traduit ses décisions en HTML réel.

React Native garde exactement le même chef d'orchestre — le cœur de React, celui qui gère l'état, les props, le re-render dont on a parlé dans « Le modèle mental de React » — mais change le moteur de rendu. Au lieu de produire du HTML pour un navigateur, il produit des instructions pour les composants natifs d'iOS (UIView, UILabel…) et d'Android (View, TextView…). Le résultat : pas de page web, pas de DOM, pas de navigateur — une vraie app installable, avec les vraies animations natives, le vrai clavier natif, la vraie barre de défilement native.

🧭 Ce que ça change pour toi qui lis le code

Tout ce que tu as appris sur React reste vrai : useState fonctionne pareil, les props se passent pareil, le JSX s'écrit pareil, map sur une liste se fait pareil. La seule chose qui change, c'est le vocabulaire des balises. Au lieu de <div> tu vois <View> ; au lieu de <button> tu vois <Pressable>. Apprends la table de correspondance et 80 % de l'étrangeté disparaît.

🧭 Et Expo dans tout ça ?

Expo est une surcouche autour de React Native : un outillage (le serveur de dev, la commande de build, les mises à jour) plus une grosse bibliothèque de modules natifs prêts à l'emploi (expo-image, expo-linking, expo-router…). Halterofit est une app Expo. Tu peux voir Expo comme « React Native, piles incluses ». La navigation, elle, passe par Expo Router — c'est tout le sujet du module « Navigation : Expo Router ».

2. React (web) vs React Native : oublie div, span, p, button

Le réflexe le plus dur à perdre, quand on vient du web, c'est de taper <div>. En React Native, ces balises HTML n'existent pas. Il faut les remplacer par des composants importés depuis 'react-native'. Voici la table de traduction mentale à garder sous la main :

Web (HTML) React Native À quoi ça sert
<div> <View> La boîte / conteneur générique (mise en page)
texte brut, <span>, <p>, <h1> <Text> Afficher du texte — obligatoire pour tout texte
<button>, onClick <Pressable>, onPress Une zone qu'on peut toucher
<input> <TextInput> Un champ de saisie texte
<img> <Image> / <Image> d'expo-image Afficher une image
conteneur qui défile (CSS overflow:scroll) <ScrollView> Zone défilante (peu d'éléments)
<ul> très longue, défilement infini <FlatList> / <FlashList> Longue liste performante (virtualisée)
spinner de chargement (souvent maison) <ActivityIndicator> La roue « ça charge » native

Tu remarques un détail de nommage : en HTML les balises sont en minuscules (div) ; en React Native elles commencent par une majuscule (View). Ce n'est pas un caprice : en JSX, une balise minuscule signifie « élément natif intégré », une balise majuscule signifie « composant React que j'ai importé ». Comme View est un composant importé de 'react-native', il prend la majuscule. C'est la même règle que pour tes propres composants comme <Button> ou <ScreenContainer>.

3. Les primitives de base, une par une

« Primitive » est le mot consacré pour les briques fournies de base par React Native, celles dont tout le reste est construit. Prenons-les une à une, avec la théorie d'abord.

View — la boîte universelle

View est l'équivalent direct du <div> : une boîte invisible qui sert à regrouper et à positionner d'autres éléments. Elle ne montre rien elle-même (pas de texte, pas d'image) ; son rôle est structurel. La grande différence avec le web : une View est en Flexbox par défaut (on y revient en §4), ce qui veut dire qu'elle sait nativement empiler et aligner ses enfants. Tu vas en voir partout : c'est l'ossature de chaque écran. Dans Halterofit, l'écran d'inscription est une View qui contient une autre View (la pile de champs), qui contient les champs.

Text — TOUT texte vit dans un Text

Règle stricte et non négociable de React Native : tout texte affiché à l'écran doit être enveloppé dans un <Text>. Sur le web tu peux écrire du texte directement dans un <div> ; ici, c'est interdit. Pourquoi ? Parce que les systèmes natifs traitent le texte comme un type d'élément à part (un UILabel sur iOS) : impossible de coller des caractères dans un conteneur générique. Si tu mets du texte « nu » dans une View, l'app plante (on en reparle dans « Piège fréquent »). Autre conséquence agréable : les Text peuvent s'imbriquer pour styler un bout de phrase — tu verras dans Halterofit un Text de paragraphe contenir des Text colorés pour les liens « Terms of Service » et « Sign in ».

Pressable — la zone tactile

Sur mobile, on ne « clique » pas, on touche. Pressable est la primitive moderne pour « rendre quelque chose tactile ». Tu enveloppes n'importe quoi (un Text, une icône, une View entière) dans un Pressable, et tu lui passes un onPress — l'exact équivalent du onClick du web. Tout le reste de ton modèle d'événements React reste identique : onPress reçoit une fonction, qui s'exécute au toucher. Halterofit s'en sert par exemple pour le petit bouton « œil » qui montre/cache le mot de passe, et pour le lien « Already have an account? Sign in ».

🧭 Pressable vs les anciens « Touchable »

Dans du code plus ancien tu croiseras TouchableOpacity ou TouchableHighlight. Ce sont les ancêtres de Pressable. Pressable est la version recommandée aujourd'hui : plus souple, elle te laisse réagir finement à l'état « pressé ». Si tu vois un Touchable*, lis-le simplement comme « un bouton tactile ».

ScrollView vs FlatList / FlashList — et pourquoi la virtualisation

Un écran ne tient pas toujours dans la hauteur du téléphone : il faut pouvoir faire défiler. ScrollView est la solution simple : tu mets ton contenu dedans, ça devient défilable. Mais ScrollView a un défaut grave pour les longues listes : il rend TOUS ses enfants d'un coup, même ceux qu'on ne voit pas. Imagine une base de 800 exercices : ScrollView construirait 800 lignes en mémoire dès le départ. C'est lent à ouvrir et ça mange la RAM.

La solution est la virtualisation : ne construire en mémoire que les éléments actuellement visibles (plus un petit coussin au-dessus et en dessous), et recycler ces « cases » au fur et à mesure que tu défiles. L'utilisateur a l'impression d'une liste de 800 éléments, mais à tout instant seulement une douzaine existent vraiment. C'est le métier de FlatList (la version intégrée à React Native) et de FlashList (une version encore plus rapide, signée Shopify). Halterofit utilise FlashList pour ses longues listes — typiquement la liste d'exercices et l'historique. À toi : longue liste qui défile à l'infini → liste virtualisée ; petit contenu fixe → ScrollView suffit.

🏋️ Dans Halterofit

Quand tu liras un écran de liste et que tu verras <FlashList ... /> avec une prop data (le tableau) et une prop renderItem (comment dessiner UNE ligne), traduis mentalement : « liste performante qui ne fabrique que les lignes visibles ». Le couple data + renderItem est le même schéma pour FlatList et FlashList. C'est aussi un sujet de performance — voir le module « Notions avancées » pour le « pourquoi » côté optimisation.

TextInput — le champ de saisie « contrôlé »

TextInput est le champ de saisie. Le point théorique important, c'est le patron du composant contrôlé, déjà rencontré avec useState. L'idée : le champ ne garde pas sa propre valeur dans son coin ; c'est ton état React qui est la source de vérité. Tu branches deux fils :

  • value : ce que le champ affiche, lu depuis ton état (ex. value={email}).
  • onChangeText : appelé à chaque frappe, avec le nouveau texte, pour mettre ton état à jour (ex. onChangeText={setEmail}).

Note la différence avec le web : là-bas l'événement s'appelle onChange et te donne un objet-événement (e.target.value). Ici c'est onChangeText et il te donne directement la chaîne — plus simple. La boucle est : l'utilisateur tape → onChangeText met à jour l'état → l'état redonne value au champ → l'écran se redessine. Le champ et l'état restent toujours synchronisés.

Image / expo-image, et ActivityIndicator

Image affiche une image. Tu lui passes une source (le fichier ou l'URL) et un style avec la taille. Halterofit affiche son logo-mot (« wordmark ») ainsi. Dans une vraie app on préfère souvent expo-image, une version améliorée fournie par Expo : meilleure mise en cache, fondu d'apparition, performance accrue — utile pour des images distantes (photos d'exercices, avatars). Quand tu vois un import depuis 'expo-image', lis-le comme « Image, en mieux ».

ActivityIndicator, enfin, est le petit spinner natif « ça charge ». Tu l'affiches pendant une opération asynchrone (une requête réseau) et tu le caches quand c'est fini. Halterofit le montre dans le bouton « Create Account » pendant l'inscription : tant que ça travaille, le bouton affiche la roue ; sinon il affiche son texte.

4. Le style en React Native : pas de fichier CSS

Gros choc culturel pour qui vient du web : il n'y a pas de fichiers .css. On ne sépare pas « le HTML d'un côté, le CSS de l'autre ». Le style se déclare en JavaScript, sous deux formes que tu croiseras toutes les deux dans Halterofit.

Forme A — l'objet de style avec StyleSheet.create. Tu décris le style comme un objet JavaScript (les propriétés ressemblent au CSS mais en camelCase : backgroundColor au lieu de background-color), et tu le passes via la prop style. StyleSheet.create regroupe ces objets proprement. Voici le pied de page de l'app, la barre d'onglets, écrit ainsi :

apps/mobile/src/app/(app)/(tabs)/_layout.tsx
// Les styles sont des OBJETS JS, pas du CSS. Propriétés en camelCase.
const styles = StyleSheet.create({
  tabBar: {
    backgroundColor: Colors.background.surface,
    borderTopWidth: 1,                 // équivaut à border-top-width: 1
    borderTopColor: Colors.background.elevated,
    height: TAB_BAR_HEIGHT,
    paddingBottom: TAB_BAR_PADDING_BOTTOM,
    paddingTop: TAB_BAR_PADDING_TOP,
  },
  tabLabel: {
    fontSize: FONT_SIZE_LABEL,
    fontWeight: FONT_WEIGHT_SEMIBOLD,
  },
});

// On l'applique ensuite via la prop `style` : tabBarStyle: styles.tabBar

Remarque la View racine de ce fichier : style={{ flex: 1, backgroundColor: ... }} — un style « en ligne » écrit directement dans la balise. flex: 1 veut dire « occupe tout l'espace disponible », un grand classique du Flexbox.

Forme B — NativeWind (les classes Tailwind via className). Plutôt que des objets style, Halterofit utilise surtout NativeWind : tu écris des classes utilitaires Tailwind dans une prop className ("flex-1 items-center px-6"), et NativeWind les traduit en style natif. C'est la même philosophie que Tailwind sur le web, portée au mobile. Tu verras donc, côte à côte, des composants stylés par className (la majorité) et quelques-uns par style (quand on a besoin de valeurs calculées, comme une marge dépendant des encoches de l'écran).

💡 Le layout est en Flexbox, en COLONNE par défaut

Sur le web, par défaut les blocs s'empilent verticalement et tu actives Flexbox à la main. En React Native, tout est déjà en Flexbox, et la direction par défaut est la colonne (les enfants s'empilent de haut en bas), pas la ligne. Donc flex-1, items-center (centrer sur l'axe transversal), justify-between (espacer sur l'axe principal) sont tes outils de mise en page quotidiens. C'est le point de Flexbox à intérioriser pour lire la disposition des écrans.

🧭 Pour aller plus loin sur le style

On ne fait ici qu'effleurer NativeWind. Le détail — comment les classes sont organisées, comment cva gère les variantes de boutons, comment le wrapper Text combine tout — est traité dans le module « Composants & style : NativeWind + CVA ». Garde juste en tête : className = style à la Tailwind, style = objet JS classique.

5. Sous le capot : deux mondes qui se parlent

Tu n'as pas besoin de ça pour lire le code au quotidien, mais comprendre l'architecture éclaire beaucoup de « pourquoi ». Une app React Native vit dans deux mondes en parallèle :

  • Le côté JavaScript : un moteur (Hermes, chez Expo) qui exécute ton code — tes composants, ton état, ta logique. C'est là que React « pense ».
  • Le côté natif : le vrai iOS / Android, qui dessine les vues à l'écran, gère le toucher, la caméra, le stockage, etc.

Quand ton code JS dit « affiche cette View avec ce texte », ce message doit traverser jusqu'au côté natif qui, lui, dessine vraiment. Historiquement ce passage se faisait par un « pont » (le bridge) qui sérialisait les messages ; la nouvelle architecture de React Native rend ce dialogue beaucoup plus direct et rapide. Tu n'as pas à connaître les détails, mais retiens l'image : ton JS décide, le natif exécute, et il y a une frontière entre les deux.

💡 Pourquoi certaines libs sont « natives »

Certaines bibliothèques ne sont pas que du JavaScript : elles embarquent aussi du code natif (Swift/Kotlin/C++) parce qu'elles ont besoin de capacités que le JS seul n'a pas, ou de vitesse. Deux exemples présents dans Halterofit : MMKV (le stockage clé-valeur ultra-rapide, écrit en C++ — voir « État global : Zustand + MMKV ») et Reanimated (les animations qui tournent directement côté natif, donc fluides même si le fil JS est occupé). On appelle ça des modules natifs : du code qui vit du côté natif et que ton JS peut appeler. C'est la raison pratique pour laquelle tu ne peux pas toujours tester ce genre de lib dans un simple navigateur.

6. Les différences de plateforme : iOS vs Android

Une même app tourne sur deux systèmes qui ne se comportent pas toujours pareil. React Native te donne un outil simple pour t'adapter : l'objet Platform.

  • Platform.OS est une chaîne : 'ios', 'android' (ou 'web'). Tu peux donc écrire des conditions if dessus.
  • Platform.select({ ... }) choisit une valeur selon la plateforme — pratique pour donner une valeur différente par OS sans empiler les if.

Dans l'écran d'inscription de Halterofit, on s'en sert pour le clavier. Sur iOS, on veut que la vue remonte ('padding') quand le clavier apparaît, pour ne pas masquer le champ ; sur Android, le système gère ça autrement, donc on ne force rien (undefined) :

<KeyboardAvoidingView
  // Sur iOS on pousse le contenu vers le haut ; sur Android, comportement par défaut.
  behavior={Platform.OS === 'ios' ? 'padding' : undefined}
  className="flex-1"
>

KeyboardAvoidingView est la View spéciale « ne te laisse pas cacher par le clavier ». Le Platform.OS === 'ios' ? ... : ... est un simple ternaire JavaScript : « si on est sur iOS, alors 'padding', sinon undefined ».

🧭 Platform.select aussi pour le style

Tu reverras Platform.select dans le wrapper Text de l'app : certaines classes (comme rendre le texte sélectionnable) ne sont appliquées que web. C'est la même idée : « cette valeur-là, seulement sur telle plateforme ».

7. Mise en pratique : décortiquer sign-up.tsx

Mettons tout ensemble sur un vrai morceau de l'écran d'inscription. Voici son cœur, légèrement raccourci. Lis-le en repérant chaque primitive ; les commentaires pointent les briques.

apps/mobile/src/app/(auth)/sign-up.tsx
return (
  <ScreenContainer>
    {/* View spéciale : empêche le clavier de cacher les champs. */}
    <KeyboardAvoidingView
      behavior={Platform.OS === 'ios' ? 'padding' : undefined}
      className="flex-1"
    >
      {/* View = la boîte. Flexbox : centré, du padding (style NativeWind). */}
      <View className="flex-1 items-center pt-52 px-6">
        {/* Image = le logo-mot. source + style (taille en objet JS). */}
        <Image source={WORDMARK} style={{ width: 240, height: 40 }} resizeMode="contain" />

        {/* La pile de champs : une View qui empile (colonne) avec un écart (gap-4). */}
        <View className="w-full max-w-sm gap-4">
          {/* TextInput contrôlé : value lue de l'état, onChangeText le met à jour. */}
          <Input
            placeholder="Email"
            value={email}
            onChangeText={setEmail}
            editable={!isLoading}      {/* champ gelé pendant le chargement */}
          />

          {/* Affichage conditionnel : on ne montre le Text d'erreur que s'il y en a une. */}
          {error !== '' && <Text className="text-destructive">{error}</Text>}

          {/* Pressable (via Button) : onPress remplace onClick. */}
          <Button onPress={handleSignUp} disabled={isLoading} size="lg">
            {isLoading
              ? <ActivityIndicator color={Colors.primary.foreground} />  /* ça charge */
              : <Text>Create Account</Text>}                          {/* sinon le libellé */}
          </Button>
        </View>
      </View>
    </KeyboardAvoidingView>
  </ScreenContainer>
);

Note que Input et Button sont des composants maison de Halterofit (majuscule), bâtis par-dessus TextInput et Pressable pour porter le style de l'app. Sous le capot, ce sont bien les primitives natives.

Relis ce bloc et tu retrouves toute la grammaire React que tu connais déjà : le JSX, les props (value, onPress), l'affichage conditionnel avec && et le ternaire ? : (vu dans « Le modèle mental de React »), l'état via useState (email, isLoading… vus dans « useState & useEffect »). Le seul changement, encore une fois, c'est le nom des balises. C'est ça, la bonne nouvelle de React Native.

✍️ Exercice de lecture

Voici une ligne tirée du même écran. Lis-la et réponds aux questions sans regarder le corrigé :

{isLoading ? (
  <ActivityIndicator color={Colors.primary.foreground} />
) : (
  <Text>Create Account</Text>
)}

Questions : (1) Que voit l'utilisateur dans le bouton quand isLoading vaut true ? (2) Pourquoi le mot « Create Account » est-il enveloppé dans un <Text> et pas écrit nu ? (3) De quelle construction JavaScript s'agit-il, ? : ?

Voir le corrigé

(1) Quand isLoading est true, le bouton affiche l'ActivityIndicator — le petit spinner natif « ça charge ». Dès que l'inscription se termine (succès ou erreur), isLoading repasse à false et le bouton réaffiche son texte. C'est de l'affichage conditionnel piloté par l'état React.

(2) Parce que la règle stricte de React Native impose que tout texte soit dans un <Text>. « Create Account » écrit nu à cet endroit ferait planter l'app.

(3) C'est un ternaire : condition ? valeurSiVrai : valeurSiFaux. C'est du JavaScript pur, identique à ce que tu ferais sur le web — seul le contenu (des composants natifs) est spécifique à React Native.

🧠 Quiz éclair

1. Par quoi remplace-t-on <div> et <button> en React Native ?

<div> devient <View> (la boîte / le conteneur de mise en page) et <button> devient <Pressable> (la zone tactile, avec onPress au lieu de onClick).

2. Quelle est la règle stricte concernant le texte ?

Tout texte affiché DOIT être à l'intérieur d'un <Text>. Mettre du texte brut dans une <View> fait planter l'app.

3. Pourquoi préfère-t-on FlatList/FlashList à ScrollView pour une longue liste ?

Parce qu'elles sont virtualisées : elles ne fabriquent en mémoire que les éléments visibles (et un petit coussin), au lieu de tout construire d'un coup comme ScrollView. Pour 800 exercices, c'est la différence entre fluide et lent. Halterofit utilise FlashList.

4. Dans un TextInput « contrôlé », à quoi servent value et onChangeText ?

value affiche ce que contient ton état React (la source de vérité) ; onChangeText est appelé à chaque frappe avec le nouveau texte, pour mettre cet état à jour. Le champ et l'état restent ainsi synchronisés.

5. Que fait Platform.OS === 'ios' ? 'padding' : undefined ?

C'est une adaptation par plateforme : sur iOS on choisit le comportement 'padding' (pour le KeyboardAvoidingView), sur tout autre OS on ne force rien (undefined). Platform.OS dit sur quel système on tourne.

À retenir

React Native = le même React (composants, props, JSX, hooks) qui pilote des vues natives au lieu du HTML. Tu remplaces les balises : divView, texte→Text (obligatoire !), buttonPressable (onPress), inputTextInput (contrôlé : value + onChangeText), longue liste→FlashList (virtualisée). Le style se fait en JS — objets StyleSheet.create ou, dans Halterofit, classes NativeWind dans className — et le layout est en Flexbox, en colonne par défaut.

⚠️ Piège fréquent : tes réflexes du web

Le piège n°1 : du texte brut hors d'un <Text>. Écrire <View>Bonjour</View> fait planter l'app (l'erreur dit quelque chose comme « Text strings must be rendered within a <Text> component »). Autres réflexes du web qui ne marchent pas : pas de <div>/<span> ; onClick n'existe pas (c'est onPress) ; pas de fichiers .css ni de pseudo-classes :hover (un téléphone ne « survole » pas) ; les unités px ne s'écrivent pas (les nombres sont déjà des unités indépendantes de la densité). Si un réflexe web te démange, demande-toi d'abord : « existe-t-il vraiment sur mobile ? »

🔄 Transférable

La grande leçon : React est un modèle, pas une cible. Le même cœur (état, props, re-render, hooks) peut piloter le web (ReactDOM), le mobile natif (React Native), et même d'autres surfaces. Comprendre que « le rendu est interchangeable » vaut bien au-delà de Halterofit : c'est ce qui te permettra demain de passer du web au mobile — ou l'inverse — sans réapprendre à penser. Et la distinction « mon code (JS) décide / la plateforme exécute » est, elle aussi, une idée d'architecture qu'on retrouve partout en informatique.