Accueil / Articles / Les compromis de React Native à grande échelle : l’état, les listes, les appareils, les tokens, les mises à jour

Les compromis de React Native à grande échelle : l’état, les listes, les appareils, les tokens, les mises à jour

Cinq problèmes de React Native qui mettent à l’épreuve le jugement technique : l’état serveur contre l’état client, les FlatLists lents, les appareils Android de basse gamme, le stockage des tokens et les mises à jour incrémentielles.

1719 mots

Créer une interface dans React Native est simple. La partie difficile apparaît à mesure que l’application se développe : l’état se répand partout, les listes fonctionnent de manière intermittente, les téléphones Android bon marché plantent, les tokens sont stockés en texte brut et le framework reste des années en retard. Ci-dessous figurent cinq situations de ce type, souvent utilisées pour évaluer les ingénieurs seniors lors d’entretiens, accompagnées des éléments essentiels qu’une réponse solide doit contenir, afin que vous puissiez les appliquer à votre propre base de code.

Séparer l’état serveur de l’état client

Lorsque l’état local, les réponses API, les caches et les indicateurs UI partagés forment un enchevêtrement indiscipliné, la première question à se poser n’est pas « Redux ou Context ? » mais plutôt « qui est responsable de ces données ? »

Les données provenant d’une API, telles que les profils, les flux ou les listes de produits, constituent un état serveur. Elles deviennent obsolètes et doivent être récupérées à nouveau, mises en cache puis invalidées. Les intégrer dans un stockage global signifie devoir réimplémenter tout cela manuellement, en plus des indicateurs de chargement et d’erreurs. Le fragment ci-dessous illustre ce mauvais pattern.

// ❌ Server data forced into a global store —
// now YOU own caching, staleness, invalidation, loading states…
dispatch(setProducts(await fetchProducts()));

TanStack Query et RTK Query existent pour gérer cette couche. Ici, useQuery organise les données par catégorie et les maintient à jour pendant 60 secondes grâce à staleTime. La seconde partie du bloc (fusionnée en une seule dans le code source) montre ce qui reste pour un stockage global : un petit objet de session.

// ✅ Server state managed by a tool built for it
const { data, refetch } = useQuery({
  queryKey: ['products', category],
  queryFn: () => fetchProducts(category),
  staleTime: 60_000,
});// ✅ Truly shared client state — small and intentional
const useSession = create<SessionStore>((set) => ({
  user: null,
  setUser: (user) => set({ user }),
}));

Le problème restant est bien plus mineur. Les champs de formulaire, les commutateurs et les valeurs d’animation restent dans le composant, où ils consomment peu de ressources, sont isolés et disparaissent avec l’écran. Un stock global ne devrait contenir que des données nécessaires à plusieurs écrans et appartenant au client lui-même, comme l’utilisateur connecté, le thème, les flags fonctionnels ou l’état de l’interface s’étendant sur plusieurs écrans.

Que se passe-t-il lorsque tout est global

Les mises à jour ré-redessinent des écrans non liés, chaque fonctionnalité dépend de la structure du stock, les tests doivent simuler l’environnement réel, et les refacturations deviennent complexes. Un stock surdimensionné ressemble à une architecture solide mais se comporte comme de la dette technique.

Diagnostiquer un FlatList lent sans deviner

Un FlatList semble lent bien que son API soit rapide. Utiliser React.memo partout relève du hasard ; l’approche rigoureuse consiste à mesurer d’abord.

Utilisez les React DevTools (ou la bibliothèque why-did-you-render) en scrollant. Lorsque toutes les lignes se rérendent à chaque mouvement de scroll, la cause en est l’identité référentielle : des fonctions flèche inline dans renderItem, des objets de style reconstruits à chaque rendu, ou l’absence de keyExtractor, ce qui force des remontages. Ici, chaque rendu crée une nouvelle fermeture onPress et un nouvel objet style, de sorte qu’aucune ligne ne peut être exemptée du rendu.

// ❌ New function + new object on EVERY render → every row re-renders
<FlatList
  data={items}
  renderItem={({ item }) => (
    <Row item={item} onPress={() => open(item.id)} style={{ padding: 12 }} />
  )}
/>

La solution consiste à fournir à React des références stables : un Row mémorisé, un renderItem enveloppé dans useCallback, une clé stable, ainsi que getItemLayout afin que la liste ne mesure pas les lignes. Les props typés rendent ce code TSX.

// ✅ Stable identities + memoized rows
const Row = React.memo(({ item, onPress }: RowProps) => { /* … */ });const renderItem = useCallback(
  ({ item }: ListRenderItemInfo<Item>) => <Row item={item} onPress={handlePress} />,
  [handlePress],
);<FlatList
  data={items}
  renderItem={renderItem}
  keyExtractor={(item) => item.id}
  getItemLayout={(_, index) => ({
    length: ROW_HEIGHT,
    offset: ROW_HEIGHT * index,
    index,
  })}
/>

getItemLayout ne convient qu’aux lignes de hauteur fixe ; des valeurs incorrectes pour les hautesurs variables provoquent des sauts et des espaces vides.

Lecture du Perf Monitor

Si les rendus semblent corrects mais que les frames disparaissent toujours, comparez les deux compteurs du Perf Monitor :

  • Un faible JS FPS indique que le thread JavaScript est surchargé, généralement à cause d’une fonction renderItem gourmande ou d’un événement onScroll non limité.
  • Un faible UI FPS révèle des coûts liés au système natif, souvent dus aux images. La décodage de photos en haute résolution pour en faire des miniatures de 80 pt consomme de la mémoire et des frames ; redimensionnez-les côté serveur ou utilisez expo-image ou FastImage avec des dimensions appropriées.

Seulement ensuite, ajustez la liste avec windowSize, removeClippedSubviews ou en passant à FlashList. Ajuster les propriétés de la liste avant d’analyser les lignes ne fait que traiter les symptômes.

Détecter les plantages sur des appareils Android à budget

Une application qui fonctionne parfaitement sur des téléphones haut de gamme peut planter sur les smartphones Android bon marché que possèdent la plupart des utilisateurs. Commencez par analyser les données : la Play Console ou les outils d’analyse montrent la réelle composition des appareils utilisés, qui correspond rarement à celle des téléphones de votre équipe.

Faites en sorte que le matériel bas de gamme fasse partie du travail quotidien : gardez un appareil physique doté de 2 à 3 GB de RAM à portée de main, ou utilisez Firebase Test Lab en fonction des modèles identifiés par vos outils d’analyse, comme le fait cette commande.

gcloud firebase test android run \
  --app app-release.apk \
  --device model=a10,version=29 \
  --device model=redmi9,version=30   # the phones in your analytics, not yours

Testez toujours les versions publiées. Les versions de débogage masquent la véritable performance, et Hermes fonctionne différemment en mode publication par rapport au mode débogage. Les pannes fréquentes sur du matériel faible sont dues à une pression mémoire causée par de grandes images ou des listes non vidées, à un thread principal surchargé, ainsi qu’à des plantages dus au manque de mémoire, phénomènes qui n’ont jamais lieu sur les téléphones haut de gamme.

Fonctionner correctement en cas de dégradation plutôt que de planter

Vous pouvez adapter l’expérience en fonction de chaque appareil. react-native-device-info indique la mémoire totale.

import DeviceInfo from 'react-native-device-info';

Avec cet outil, vous pouvez qualifier les appareils disposant de moins de 3 Go comme étant bas de gamme, afficher des miniatures au lieu d’images complètes, et omettre les effets de flou, le parallaxe ainsi que les animations lourdes. Comme getTotalMemory est asynchrone, calculez ce critère une seule fois au démarrage plutôt que d’attendre pendant le rendu.

const totalMemory = await DeviceInfo.getTotalMemory();
const isLowEnd = totalMemory < 3 * 1024 ** 3; // < 3 GB RAM<Image
  source={{ uri: isLowEnd ? item.thumbUrl : item.fullResUrl }}
  // skip blur, parallax and heavy animations on low-end devices
/>

Ensuite, procédez de manière prudente : segmentez les rapports de Sentry ou Crashlytics par catégorie d’appareil, utilisez des déploiements progressifs sur le Play Store (5 %, 20 %, 100 %), et arrêtez tout avant qu’une version défectueuse n’atteigne tous les utilisateurs. L’objectif n’est pas d’éviter complètement les pannes, mais de les détecter tôt et à moindre coût.

Stockage sécurisé des tokens d’authentification

AsyncStorage enregistre les données sans chiffrement sur le disque : un fichier SQLite sur Android, des fichiers classiques dans le sandbox sur iOS. Un appareil rooté ou jailbréké, ainsi qu’une sauvegarde malveillante ou un accès au système de fichiers, exposent directement les tokens. Il a été conçu pour stocker des préférences, pas des secrets.

Les tokens doivent être stockés dans un espace de stockage protégé par du matériel, comme le Keychain d’iOS et Keystore d’Android, que react-native-keychain encapsule.

import * as Keychain from 'react-native-keychain';

Cette appelée stocke les tokens sérialisés avec l’option WHEN_UNLOCKED_THIS_DEVICE_ONLY : ils ne peuvent être lus que tant que l’appareil est déverrouillé et ne sont jamais migrés vers un autre appareil.

await Keychain.setGenericPassword('auth', JSON.stringify(tokens), {
  accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});

Un flux de mise à jour capable de gérer des erreurs 401 simultanées

Attribuez aux tokens d’accès une durée de validité de quelques minutes plutôt que de jours, soutenez-les par un token de renouvellement qui est remplacé à chaque fois qu’il est échangé, et centralisez tout cela dans une couche d’authentification qui effectue le renouvellement exactement une fois, quel que soit le nombre de demandes échouées en même temps. Une promesse partagée rend cela possible.

let refreshing: Promise<string> | null = null;

L’intercepteur d’Axios rejette tout sauf les codes 401. Pour un code 401, ??= déclenche un renouvellement uniquement s’il n’y en a pas en cours, de sorte que les échecs simultanés attendent la même promesse ; finally la réinitialise, et la demande originale est renouvelée avec le nouveau token d’accès. Le code source regroupe plusieurs instructions sur une seule ligne. Pour en savoir plus, consultez pourquoi les codes 401 simultanés déconnectent les utilisateurs et la solution du renouvellement unique.

api.interceptors.response.use(undefined, async (error) => {
  if (error.response?.status !== 401) throw error;  // Concurrent 401s all await the SAME refresh — no refresh storm
  refreshing ??= refreshTokens().finally(() => (refreshing = null));
  const newToken = await refreshing;  error.config.headers.Authorization = `Bearer ${newToken}`;
  return api.request(error.config); // retry the original request
});

Deux détails supplémentaires : ne conservez pas les tokens dans un état global accessible depuis JavaScript plus longtemps que nécessaire, ajoutez le pinning de certificats pour les API vraiment sensibles, et n’ayez jamais recours à l’enregistrement des tokens, car les outils de rapport d’erreurs capturent volontiers les en-têtes des requêtes. « Utilisez SecureStore » désigne un outil ; une réponse solide couvre les modes de vie utile, de renouvellement et d’échec.

Mettre à jour une application React Native vieille de deux ans

L’application est dépassée depuis deux ans, aucun temps d’arrêt n’est toléré et il n’y a pas de budget pour une réécriture.

N’allez jamais directement à la dernière version. L’outil React Native Upgrade Helper affiche les différences exactes entre les versions ; avancez d’une ou deux versions mineures à la fois, en veillant à ce que l’application reste compilable et prête à être déployée en permanence. Chaque étape représente une publication ordinaire, ce qui permet d’éviter tout temps d’arrêt.

Faites d’abord un audit des dépendances

Les mises à niveau échouent en raison de bibliothèques natives anciennes et non maintenues, et non à cause de React Native lui-même. Avant le premier pas, identifiez les dépendances qui bloquent la Nouvelle Architecture ou les exigences plus récentes de Gradle et Xcode, puis remplacez-les ou créez des versions alternatives pour celles qui ont été abandonnées.

Automatisez la vérification de chaque étape

Mettez en place des tests de fonctionnement intégraux pour les flux critiques avant de commencer, afin que chaque étape soit vérifiée en quelques minutes plutôt qu’au moyen d’un contrôle qualité manuel. Ce flux Maestro se connecte à l’aide d’une adresse e-mail provenant d’une variable d’environnement et vérifie que les pages d’accueil, de panier et de paiement sont accessibles.

# smoke-test.yaml — run with Maestro on every upgrade hop
appId: com.myapp
---
- launchApp
- tapOn: "Log in"
- inputText: ${EMAIL}
- tapOn: "Continue"
- assertVisible: "Home"
- tapOn: "Cart"
- assertVisible: "Checkout"

Présentez ce travail à la direction en termes de réduction des risques, et non de refactoring : chaque version manquée augmente le coût de la prochaine mise à niveau forcée due aux règles du magasin, aux dépréciations des systèmes d’exploitation et aux correctifs de sécurité. De petits progrès transforment un projet intimidant en une série de mises à jour ennuyeuses, et l’ennui est justement l’objectif visé.

Points clés

  • Décidez qui possède chaque élément de données ; laissez une bibliothèque de requêtes gérer l’état du serveur afin de maintenir le stockage global compact.
  • Faites un profilage avant d’optimiser : les problèmes d’identité, le travail sur des threads JavaScript et la décodage d’images nécessitent des solutions différentes.
  • Testez les versions à publier sur les appareils réels de vos utilisateurs et déploiez-les par étapes.
  • Traitez les tokens comme un système : stockage sécurisé, durée de vie courte, rotation et mise à jour unique.
  • Mettez à niveau par de petits pas réalisables, soutenus par des tests automatisés de base.