Accueil / Articles

Raisonnement sur les Hooks de React à travers les captures d’écran de rendu et les compromis.

Un modèle mental de niveau avancé pour React et les Hooks de React Native : captures d’écran de rendu, effets, refs, mémorisation, Hooks personnalisés, ainsi que la manière d’expliquer les compromis lors des entretiens.

Éditeur : REACTAPP.TOP26 min de lecture6055 mots

Sources: React & React Native Hooks: The Complete Senior Interview Guide

Les entretiens avancés pour React et React Native ne se limitent rarement à la question « Que fait ce Hook ? ». Une question sur useState mène en moins d’une minute à des discussions sur le regroupement des mises à jour et les mises à jour fonctionnelles ; une question sur useEffect débouche sur des échanges concernant les boucles infinies, les closures obsolètes, la possibilité de canceler des effets et les cas où un effet ne devrait pas exister du tout ; useMemo et useCallback amènent directement à la question « Est-ce que cela rend vraiment les choses plus rapides ? ». Les définitions mémorisées ne résistent pas à ce type de questions approfondies. Ce qui survit, c’est un modèle cohérent, et ce guide en construit un : le rendu, puis l’état, les closures, les effets, les refs, la mémorisation, les Hooks personnalisés, les performances, et enfin l’architecture, avec des exemples React Native là où la plateforme modifie le schéma général.

Le modèle de rendu sur lequel tout le reste repose

Partez d’une seule idée et tenez-vous-y : Lorsque React rend un composant, il appelle à nouveau la fonction du composant depuis le début. Utilisez un compteur aussi petit que possible.

function Counter() {
  const [count, setCount] = useState(0);
return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}

Il est naturel d’imaginer que cliquer sur le bouton permet d’accéder au composant et de modifier la variable count. Ce n’est pas ce qui se passe. Le setter demande à React de re-renderiser le composant, et la prochaine appelation de Counter reçoit la nouvelle valeur en tant que constante fraîche.

Render 1
count = 0
     ↓
User clicks
     ↓
setCount(1)
     ↓
React schedules another render
     ↓
Render 2
count = 1

Chaque rendu possède donc une capture de ses props et de son état. Rien à l’intérieur de cette capture ne change par la suite. Cette simple observation explique les fermetures sur des valeurs anciennes, l’état obsolète dans les timers, l’existence des dépendances effect, le comportement des callbacks et des gestionnaires d’événements, ainsi que le fait que la mémorisation concerne l’identité. Les interviewers qui vont au-delà de la surface de l’API cherchent généralement à savoir si vous maîtrisez ce modèle.

useState : l’état qui appartient au rendu

useState fournit à un composant fonctionnel une partie d’état qui alimente sa sortie.

const [count, setCount] = useState(0);

La paire retournée a deux rôles distincts : une valeur pour le rendu actuel et une fonction qui demande une mise à jour.

 count
   ↓
current state


 setCount
   ↓
request a state update

Appeler le setter ne modifie rien sur place ; il planifie un autre rendu au cours duquel le composant verra la nouvelle valeur.

Comment les questions s’intensifient

Une progression typique se présente comme suit :

  • De base : quel problème useState résout-il ?
  • Intermédiaire : en quoi l’état diffère-t-il d’une variable locale ordinaire ?
  • Avancé : quand l’fonction mise à jour est-elle le choix approprié ?
  • Plus approfondi : que produit réellement l’expression setCount(count + 1), répétée trois fois dans un même gestionnaire d’événements ?

« Il stocke l’état » ne répond qu’à la première question. Une réponse plus complète prend en compte le modèle global :

useState conserve une valeur qui influence ce que le composant affiche. Appeler la fonction de mise à jour enfile une nouvelle affichage avec la valeur mise à jour ; par conséquent, il est préférable de considérer l’état comme faisant partie du snapshot de cet affichage, et non comme une variable à modifier. Lorsque la valeur suivante dépend de la précédente, l’fonction mise à jour constitue le choix sûr, car React regroupe les mises à jour et la valeur capturée peut déjà être obsolète.

En revanche, une variable ordinaire est recréée avec sa valeur initiale à chaque rendu et en la modifiant on ne demande jamais à React de se rérender à nouveau, c’est pourquoi elle ne peut pas piloter l’interface utilisateur.

Pourquoi les mises à jour fonctionnelles sont importantes

Voici trois incréments écrits en fonction de la valeur capturée.

setCount(count + 1);
setCount(count + 1);
setCount(count + 1);

Tous les trois lisent le même count à partir du snapshot actuel, donc après regroupement le résultat est un seul incrément, et non trois. La forme d’actualisation corrigée résout ce problème :

setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);

React transmet à chaque actualiseur la valeur en attente la plus récente, de sorte que les trois appels s’enchaînent correctement. Utilisez cette forme chaque fois que plusieurs mises à jour ont lieu en même temps ou sont en file d’attente, chaque fois qu’un callback pourrait s’exécuter avec un snapshot ancien, et en général chaque fois que la nouvelle valeur est calculée à partir de l’ancienne.

Un panier d’achat dans une application React Native constitue un cas concret. La quantité y est affichée, ce qui signifie qu’elle doit faire partie de l’état :

const [quantity, setQuantity] = useState(1);

Les gestionnaires dérivent la valeur suivante à partir de la précédente, et la fonction de décrémentation limite cette valeur à 1 afin que le panier ne montre jamais zéro ou une quantité négative.

const increaseQuantity = () => {
  setQuantity(prev => prev + 1);
};const decreaseQuantity = () => {
  setQuantity(prev => Math.max(1, prev - 1));
};

Rien ne change entre le web et les applications natives

Le Hook est identique sur les deux plateformes. Un menu déroulant en version web peut par exemple suivre son état d’ouverture :

const [isOpen, setIsOpen] = useState(false);

Une page React Native peut suivre la visibilité d’un modal de la même manière :

const [isModalVisible, setModalVisible] = useState(false);

Le mécanisme d’état est partagé ; seules les primitives UI qu’il contrôle diffèrent.

useEffect : synchronisation avec le monde extérieur

Aucun Hook ne génère autant de discussions lors d’interviews. La description courante, « il exécute du code après le rendu », est vraie mais omet son but. Une formulation plus appropriée serait que un effet maintient votre composant en synchronisation avec un système que React ne contrôle pas.

Sur le web, ce système peut être des événements DOM, des API du navigateur, des WebSockets, des abonnements, des compteurs temporels ou des requêtes réseau. Dans React Native, il s’agit souvent de AppState, du module Keyboard, des modules natifs, des écouteurs de notifications push, de l’état réseau, des API du dispositif, des WebSockets et des compteurs temporels.

Comment les questions évoluent

  • Basique : à quoi sert useEffect ?
  • Intermédiaire : expliquez le fonctionnement du tableau de dépendances.
  • Avancé : qu’est-ce qui fait qu’un effet tourne indéfiniment ?
  • Très avancé : dans quels cas un effet est-il l’outil inadapté ?
  • En production : comment annuler un abonnement WebSocket ?

Comparez « useEffect est destiné aux appels API » avec ceci :

useEffect existe principalement pour maintenir un composant synchronisé avec un système externe. Le tableau des dépendances liste les valeurs réactives que l’effet lit, et il doit être précis ; la règle de linting des Hooks détecte les omissions. Lorsque l’effet ouvre une abonnement, démarre un chronomètre ou ajoute un écouteur, il retourne une fonction de nettoyage afin que la ressource ancienne soit libérée avant que l’effet ne s’exécute à nouveau ou lorsque le composant est démonté.

Lecture du tableau des dépendances

Avec une dépendance, l’effet s’exécute après la première rendu et à nouveau chaque fois que cette valeur change entre les rendus.

useEffect(() => {
  fetchUser(userId);
}, [userId]);

En l’absence totale de tableau, il s’exécute après chaque rendu :

useEffect(() => {
  // ...
});

Avec un tableau vide, il s’exécute après le montage du composant :

useEffect(() => {
  // ...
}, []);

Faites attention à l’expression « s’exécute une fois ». En mode développement, le Mode Strict monte délibérément l’effet, le nettoie puis le monte à nouveau afin de mettre en évidence les effets qui ne se nettoient pas correctement. L’affirmation précise est qu’un effet utilisant un tableau vide s’exécute après le montage, et que le Mode Strict en développement peut ajouter un cycle de configuration et de nettoyage supplémentaire. Le code qui ne fonctionne pas suite à cette double exécution présente généralement une véritable erreur.

Le nettoyage empêche les fuites de ressources

userId ouvre une connexion dans le corps de l’effet :

useEffect(() => {
  const subscription = subscribeToUpdates(userId);

et renvoie une fonction qui la ferme :

  return () => {
    subscription.unsubscribe();
  };
}, [userId]);

Lorsque userId change, React exécute le nettoyage précédent avant de lancer le nouvel effet, de sorte qu’il n’y a jamais plus d’un abonnement actif à la fois :

Old effect
    ↓
cleanup old subscription
    ↓
new effect
    ↓
subscribe using new userId

Lors du démontage, le dernier nettoyage s’exécute et rien n’est laissé derrière :

component unmounts
        ↓
cleanup
        ↓
subscription removed

Le même mécanisme gère l’annulation des tâches asynchrones. Pour une requête fetch, vous pouvez interrompre la demande à l’aide d’un AbortController lors du nettoyage, ou définir un drapeau ignore afin qu’une réponse tardive provenant d’un ancien userId ne supprime pas des données plus récentes.

Exemple React Native : réagir à AppState

Un effet plus convaincant qu’une simple récupération de données est celui qui lie le composant au cycle de vie de l’application native. Ce écouteur met à jour les données lorsque l’application revient en premier plan et se désactive lors du démontage.

useEffect(() => {
  const subscription = AppState.addEventListener(
    "change",
    nextState => {
      if (nextState === "active") {
        refreshData();
      }
    }
  );
return () => {
    subscription.remove();
  };
}, []);

C’est là la forme la plus claire de synchronisation avec un système externe, et cela montre à l’intervieweur que vous comprenez l’objectif des effets.

Erreure : synchroniser des valeurs dérivées via un effet

Supposons qu’un formulaire stocke le prénom et le nom :

const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");

Un anti-pattern fréquent consiste à ajouter un troisième élément d’état ainsi qu’un effet pour le mettre à jour :

const [fullName, setFullName] = useState("");
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

Cela entraîne un état redondant et une rendu supplémentaire chaque fois qu’un nom change, de sorte que pour un rendu donné, le nom complet est obsolète. Comme la valeur dépend entièrement de l’état existant, il convient de la calculer lors du rendu :

const fullName = `${firstName} ${lastName}`;

Le principe à retenir : si une valeur peut être calculée de manière synchrone à partir des props ou de l’état, il faut la dériver lors du rendu plutôt que de la stocker puis de la mettre à jour via un effet. Ce pattern est abordé plus en détail dans arrêter la synchronisation de l’état avec useEffect.

Erreur : dépendances d’objets instables

Ici, un objet littéral est créé à l’intérieur du composant et listé comme dépendance.

const filters = {
  status: "active",
};

useEffect(() => {
  fetchUsers(filters);
}, [filters]);

Chaque rendu crée un nouvel objet filters, et les dépendances sont comparées par référence, ce qui fait que l’effet perçoit un « changement » à chaque fois. Si l’effet met à jour l’état, le cycle se nourrit lui-même :

Render
 ↓
new filters object
 ↓
dependency changed
 ↓
effect
 ↓
setState
 ↓
render
 ↓
new filters object
 ↓
effect again

La solution réflexive consiste à envelopper l’objet dans une mémoisation :

useMemo(...)

Avant de le faire, demandez-vous si l’objet a vraiment besoin d’être créé à l’intérieur du composant. Une constante peut être déplacée en dehors du composant ; une valeur construite à partir des props peut souvent être remplacée en s’appuyant sur ses champs primitifs (par exemple status) ou en la construisant à l’intérieur de l’effet. Corriger le flux de données est la solution la plus appropriée ; ajouter une mémoisation pour le cacher ne l’est pas.

useRef : une mémoire qui ne se rend pas

useRef retourne un conteneur mutable qui persiste entre les rendus et dont les modifications ne déclenchent jamais un nouveau rendu.

const valueRef = useRef(0);

On lit et on écrit son contenu via la propriété current :

valueRef.current

En assignant une nouvelle valeur, comme ci-dessous, on met à jour le conteneur mais l’écran reste tel qu’il était jusqu’à ce que quelque chose d’autre provoque une mise à l’écran.

valueRef.current = 10;

Les interviewers passent généralement de « en quoi une référence diffère-t-elle de l’état ? » à « donnez un exemple concret » puis à « pourquoi choisir une référence plutôt que l’état dans ce cas ? ». Une réponse solide :

L’état est des données qui alimentent la mise à l’écran, donc en le modifiant on planifie une mise à jour. Une référence conserve une valeur mutable entre les mises à l’écran sans effectuer de mise à jour lorsque .current change. Les références conviennent aux identifiants impératifs, aux IDs de timer, aux valeurs précédentes et à d’autres données mutables que l’interface utilisateur ne affiche pas.

Sur le web, une référence permet un accès impératif à un nœud DOM, par exemple pour mettre en surbrillance un champ de saisie :

const inputRef = useRef<HTMLInputElement>(null);

inputRef.current?.focus();

Dans React Native, les refs sont tout aussi courantes, car les interfaces utilisateur mobiles ont souvent besoin d’appels impératifs tels que remonter une liste en haut :

const scrollRef = useRef<ScrollView>(null);

scrollRef.current?.scrollTo({
  y: 0,
  animated: true,
});

Choix entre l’état et une ref

La question décisive est simple : le changement de cette valeur doit-il modifier ce que voit l’utilisateur ? Si oui, choisissez

useState

et si non, choisissez

useRef

La tab active affecte évidemment l’interface utilisateur, donc c’est de l’état :

const [selectedTab, setSelectedTab] = useState("home");

Un gestionnaire de timer n’existe que pour pouvoir le supprimer ultérieurement ; rien ne le rend visible, donc une ref est l’endroit approprié :

const timerRef = useRef<number | null>(null);

useMemo : mise en cache d’une valeur calculée

useMemo stocke le résultat d’un calcul et ne le recalcule que lorsque l’une de ses dépendances change. Une liste d’utilisateurs filtrée et triée en est un exemple classique :

const filteredUsers = useMemo(() => {
  return users
    .filter(user => user.active)
    .sort(sortUsers);
}, [users]);

Tant que users conserve la même référence, le filtrage et le tri sont omis lors des rendus suivants. Faites attention à une exigence cachée : sortUsers doit également être stable, ou être indiqué comme dépendance, et sort modifie l’array en place, ce qui est sécurisé ici uniquement parce que filter a déjà généré un nouvel array.

La question qui suit est presque toujours : « Devrions-nous l’utiliser partout ? ». Voici une réponse éclairée :

Non. useMemo n’est pas gratuit : il ajoute un suivi des dépendances, de la mémoire et du code à lire. Il s’avère utile lorsque des calculs coûteux sont répétés sans raison, ou lorsque la stabilité d’une référence est importante pour des éléments ultérieurs ; il convient donc de procéder à un profilage avant de l’ajouter.

Mémoriser une concaténation de chaînes de caractères est l’exemple typique d’un usage excessif :

const fullName = useMemo(
  () => `${firstName} ${lastName}`,
  [firstName, lastName]
);

Le travail est trivial, donc l’expression simple est à la fois plus rapide à lire et au moins aussi rapide à exécuter :

const fullName = `${firstName} ${lastName}`;

Considérez maintenant une application de trading en React Native affichant des milliers d’opérations, filtrées par symbole et fenêtre temporelle, et triées du plus récent au plus ancien.

const visibleTrades = useMemo(() => {
  return trades
    .filter(trade => trade.symbol === selectedSymbol)
    .filter(trade => trade.timestamp >= startTime)
    .sort((a, b) => b.timestamp - a.timestamp);
}, [trades, selectedSymbol, startTime]);

Lorsque cette transformation devient significativement coûteuse, la mémorisation s’avère utile. Même dans ce cas, confirmez-le avec un profileur plutôt que de supposer.

useCallback : conserver l’identité d’une fonction

useCallback renvoie le même objet de fonction entre les rendus tant que ses dépendances ne changent pas.

const handlePress = useCallback(() => {
  submitOrder();
}, [submitOrder]);

La différence avec useMemo réside uniquement dans ce qui est mémorisé. L’un mémorise une valeur :

useMemo
→ memoises a value

L’autre mémorise une référence à une fonction :

useCallback
→ memoises a function reference

En fait, useCallback(fn, deps) se comporte comme useMemo(() => fn, deps). « Pour améliorer les performances » est une réponse insuffisante à la question « pourquoi l’utiliser ? ». Une meilleure réponse :

useCallback maintient l’identité d’une fonction stable tant que ses dépendances restent les mêmes. Il est utile à utiliser là où l’égalité de référence a vraiment de l’importance, comme pour un callback transmis à un composant mémorisé ou une fonction que un autre Hook liste comme dépendance. Il ne devrait pas être ajouté par défaut, car la mémorisation a ses propres coûts et rend le code plus difficile à suivre.

Comment cela se connecte à React.memo

React.memo évite de rérender un composant lorsque toutes ses props sont strictement égales à celles de la fois précédente.

const UserRow = React.memo(function UserRow({
  user,
  onPress,
}) {
  return (
    // ...
  );
});

Si le parent passe une nouvelle fonction flèche à chaque rendu, une prop est toujours différente :

<UserRow
  user={user}
  onPress={() => openUser(user.id)}
/>

Comme la référence change, la comparaison superficielle échoue et la ligne mémorisée s’affiche malgré tout, ce qui peut annuler discrètement les avantages de React.memo. Un gestionnaire stable ressemble à ceci :

const handlePress = useCallback(() => {
  openUser(user.id);
}, [user.id]);

et est transmis à la place de la fonction inline :

<UserRow
  user={user}
  onPress={handlePress}
/>

Tenez compte de cette précaution : faites cela lorsque vous avez des preuves que le rendu du composant enfant est coûteux, et non par réflexe.

Fonctions fermées et le problème des valeurs obsolètes

Les fonctions fermées sont une fonctionnalité de JavaScript et non un Hook, mais il est difficile de raisonner avec les Hooks sans elles. Une fonction fermée est une fonction qui conserve un accès aux variables du contexte dans lequel elle a été créée.

function createLogger(name: string) {
  return () => {
    console.log(name);
  };
}

La fonction interne voit toujours name longtemps après le retour de createLogger.

Comment apparaît une fonction fermée obsolète dans React

Considérez un composant avec un compteur :

function Counter() {
  const [count, setCount] = useState(0);

et deux boutons, l’un pour incrémenter et l’autre pour enregistrer le compteur après un délai de une seconde :

  const handleClick = () => {
    setTimeout(() => {
      console.log(count);
    }, 1000);
  };  return (
    <>
      <button onClick={handleClick}>
        Log Count
      </button>      <button onClick={() => setCount(c => c + 1)}>
        Increment
      </button>
    </>
  );
}

Supposons que le gestionnaire d’enregistrement ait été créé lors d’une rendu où

count = 0

Si vous cliquez sur « Enregistrer le compteur » puis que vous incrémentez plusieurs fois au cours de cette seconde, le délai d’attente s’affiche quand même

0

même si les rendus suivants ont déjà continué

count = 1
count = 2
count = 3

La fonction de rappel du délai d’attente fait référence à count provenant du rendu qui l’a créée, et cette valeur ne change jamais.

Comment l’expliquer lors d’un entretien

Les composants fonctionnels fonctionnent avec des captures de rendu : chaque rendu possède ses propres valeurs et ses propres closures. Un callback créé lors d’un rendu capture les valeurs de ce rendu, ce qui permet à quelque chose d’asynchrone comme un timer d’observer l’état ancien. La solution dépend des besoins : une mise à jour fonctionnelle, la correction des dépendances d’effet, ou l’utilisation d’un ref lorsque la valeur mutable la plus récente est véritablement nécessaire sans provoquer de rendu.

Cette réponse identifie la cause du problème et propose trois solutions accompagnées des conditions requises pour chacune, ce qui correspond exactement à ce qu’un intervieweur senior cherche à entendre.

Custom Hooks : emballage du comportement

Les Custom Hooks sont le point où les idées précédentes se combinent. Un Custom Hook est une fonction dont le nom commence par use et qui appelle d’autres Hooks, vous permettant d’extraire un comportement à état et de le réutiliser. Un chargeur de profil commence par déclarer son état :

function useUserProfile(userId: string) {
  const [data, setData] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

Il exécute ensuite la requête dans un effet lié à userId et renvoie les trois valeurs :

  useEffect(() => {
    // fetch user
  }, [userId]);  return {
    data,
    loading,
    error,
  };
}

Une interface l’utilise sans comprendre le fonctionnement du chargement :

function ProfileScreen({ userId }) {
  const {
    data,
    loading,
    error,
  } = useUserProfile(userId);

et affiche ce dont elle a besoin à partir du résultat :

  // UI
}

Notez que chaque composant appelant un Hook personnalisé dispose de son propre état indépendant ; le Hook partage la logique, pas les données.

Ce que l’interlocuteur vérifie réellement

La question « pouvez-vous écrire un Hook personnalisé ? » concerne en réalité votre capacité à identifier une abstraction comportementale réutilisable. De bons candidats se distinguent ainsi :

useAuth()
useUserProfile()
useNetworkStatus()
usePagination()
useDebounce()
useKeyboard()
useAppState()
usePermissions()
useInfiniteScroll()

Dans React Native, un Hook de suivi du statut réseau en est un exemple clair. Il s’abonne aux changements de connectivité, stocke l’état correspondant et renvoie la fonction d’annulation comme méthode de nettoyage.

function useNetworkStatus() {
  const [isConnected, setIsConnected] = useState(true);

useEffect(() => {
    const unsubscribe = subscribeToNetworkChanges(
      state => {
        setIsConnected(state.isConnected);
      }
    );
return unsubscribe;
  }, []);
return {
    isConnected,
  };
}

Une interface de paiement prend ensuite une décision en fonction du résultat :

function CheckoutScreen() {
  const { isConnected } = useNetworkStatus();

if (!isConnected) {
    return <OfflineMessage />;
  }

return <Checkout />;
}

L’écran ne sait pas comment les abonnements réseau sont configurés, et c’est précisément cette ignorance qui constitue toute la valeur de l’abstraction.

Le reste de l’outilbox Hook

Les entretiens pour des postes de niveau supérieur vont souvent au-delà des cinq Hooks courants, il est donc utile de connaître l’ensemble plus large.

useContext

Il lit une valeur fournie plus haut dans la hiérarchie.

const theme = useContext(ThemeContext);

Il convient aux problématiques partagées et transversales : thème, authentification, locale et configuration. Ce n’est pas automatiquement une bibliothèque de gestion d’état ; chaque consommateur se réredessine lorsque la valeur fournie change, ce qui est important pour des données mises à jour fréquemment.

useReducer

Lorsque plusieurs éléments d’état changent en même temps, des setters séparés dispersent la logique de transition :

setLoading(...)
setData(...)
setError(...)

Un réducteur la centralise :

const [state, dispatch] = useReducer(reducer, initialState);

Les composants décrivent ce qui s’est passé et le réducteur décide de l’état suivant :

dispatch({
  type: "FETCH_SUCCESS",
  payload: data,
});

Modéliser les transitions en actions nommées permet de prédire et de tester plus facilement les états complexes.

useLayoutEffect

Il a la même structure que useEffect, mais s’exécute de manière synchrone après que React a mis à jour l’arborescence et avant que le navigateur ne redessine l’écran, ce qui est nécessaire lorsque l’on doit mesurer le layout et effectuer des ajustements avant que l’utilisateur ne remarque un clignotement. React Native présente des cas similaires où le timing du layout est important. Il ne s’agit pas d’une version plus rapide de useEffect ; puisqu’il bloque le redessin, un usage excessif nuit à la réactivité.

useId

Il génère des identifiants uniques et stables, principalement pour les relations d’accessibilité telles que le lien entre une étiquette et un champ de saisie. Il est particulièrement utile sur le web ; dans React Native, qui ne dispose pas de DOM de navigateur, ses cas d’usage sont plus limités.

useImperativeHandle

Cela permet à un composant de choisir précisément ce que révèle une référence associée, plutôt que de divulguer le nœud sous-jacent. Un composant réutilisable peut par exemple exposer la fonction de mise en surbrillance :

ref.current?.focus();

ou le défilement vers le haut :

ref.current?.scrollToTop();

Cette interface impérative contrôlée est particulièrement utile pour les composants React Native partagés.

useSyncExternalStore

C’est la méthode prise en charge pour lire dans un stockage situé en dehors de React, ce qui permet d’obtenir des captures cohérentes même en cas de rendu concurrentiel. C’est une technique avancée, mais elle est abordée dans les discussions d’architecture et constitue la base sur laquelle sont construites les bibliothèques de gestion d’état.

useTransition

Ce mécanisme marque certaines mises à jour comme non urgentes. Conceptuellement, les tâches urgentes maintiennent l’interface réactive :

Urgent update
     ↓
Keep UI responsive

alors que les tâches non urgentes peuvent être planifiées avec une priorité inférieure :

Non-urgent update
     ↓
Can be scheduled with lower priority

Cela est important dans les applications volumineuses où un re-render coûteux entre en concurrence avec la saisie ou les tapotements.

useDeferredValue

Ce composant diffère la valeur afin que les interactions urgentes restent rapides. Scénario classique :

User typing
     ↓
Keep input responsive
     ↓
Defer expensive filtering

La saisie est mise à jour immédiatement, tandis que le filtrage lourd traite une copie différée de la requête.

Les règles des Hooks

Deux règles permettent au fonctionnement des Hooks, car React identifie chaque Hook selon l’ordre de ses appels.

Appeler les Hooks au niveau le plus élevé

N’encadrez jamais un Hook dans une condition, une boucle ou un return précoce :

if (isLoggedIn) {
  useEffect(() => {});
}

Mettez plutôt la condition à l’intérieur du Hook :

useEffect(() => {
  if (!isLoggedIn) {
    return;
  }

et listez la valeur sur laquelle il dépend :

  // logic
}, [isLoggedIn]);

Appeler les Hooks uniquement depuis des fonctions React

Les Hooks doivent être utilisés dans des composants fonctionnels ou des Hooks personnalisés, et non dans des fonctions d’aide arbitraires ou des méthodes de classe.

Parler de performances

« Cet écran est lent, que feriez-vous ? » est une question courante. Répondre « ajouter useMemo et useCallback » montre un réflexe erroné. Une meilleure réponse :

La première étape consiste à déterminer où passe réellement le temps : avec quelle fréquence les composants sont rendus, quels calculs sont coûteux, où se situent les limites des composants, comment les listes sont rendues, comment les mises à jour d’état se propagent, et quelles tâches liées à la plateforme sont impliquées. Ensuite, on analyse les performances et on corrige le véritable goulot d’étranglement.

Les performances de React Native vont au-delà du rendu

Le coût d’un écran mobile est réparti sur plusieurs niveaux :

React rendering
       ↓
JavaScript execution
       ↓
Native platform work
       ↓
Layout
       ↓
Images
       ↓
Animations
       ↓
Lists
       ↓
Network

Ainsi, lorsque l’écran React Native stagne, la cause n’est pas nécessairement

useState

ou

useEffect

En aucun cas. Il peut s’agir également de tâches lourdes en JavaScript, d’un trop grand nombre de rendus, de listes volumineuses, du traitement d’images, du chargement d’animations, d’opérations natives, de latences réseau ou de abonnements à des états qui activent un trop grand nombre de composants. Savoir où chercher au sein de ces différents niveaux est ce qui distingue les experts en expérience mobile.

Une liste importante de transactions

Imaginez une application financière qui affiche des milliers de transactions. La version naïve recalcule l’ensemble du processus à chaque rendu :

const visibleTransactions = transactions
  .filter(...)
  .sort(...)
  .map(...);

Mémoriser les opérations de filtrage et de tri constitue une étape raisonnable :

const visibleTransactions = useMemo(() => {
  return transactions
    .filter(...)
    .sort(...);
}, [transactions]);

Cela ne constitue que rarement la réponse complète. Il vous faut également de la virtualisation de liste, un affichage efficace des éléments, des clés stables, des composants de ligne mémorisés, des abonnements au statut ciblés, un chargement d’images adapté et des interactions réactives. Dans React Native, choisir FlatList ou FlashList et les configurer correctement est généralement bien plus important qu’utiliser un seul useMemo. La question à se poser est de savoir où se trouve le goulot d’étranglement, et non quel Hook ajouter.

Propriété du statut et architecture

Contexte versus bibliothèque de stockage

« Quand utiliserait-on Context plutôt que Redux ou Zustand ? » ne possède pas de réponse basée sur une marque spécifique.

Cela dépend de l’état. Le contexte convient bien aux valeurs relativement stables et transversales comme le thème ou la configuration. Pour un état d’application complexe qui est mis à jour fréquemment, nécessite des sélecteurs, des transitions prévisibles ou des abonnements détaillés, une bibliothèque d’état dédiée est généralement plus adaptée.

Les concepts importants sont la propriété et la fréquence de mise à jour.

Tout ne doit pas être stocké dans un magasin global

Une erreur architecturale courante consiste à diriger toutes les valeurs vers un seul magasin :

Everything
   ↓
Global store

Demandez plutôt à qui appartient chaque élément d’état. L’état des composants locaux couvre les détails de l’interface utilisateur à courte durée de vie :

isModalOpen
inputValue
selectedTab

L’état du serveur est des données récupérées depuis une API ; il est souvent mieux géré par une bibliothèque dédiée à l’état du serveur qui gère le cache et les rechargements :

user profile
products
orders
transactions

Le état client global correspond à l’ensemble restreint de valeurs que de nombreuses interfaces partagent réellement :

theme
authentication state
application preferences

Sur le web, certaines informations d’état doivent figurer dans l’URL afin de persister après un rechargement et de pouvoir être partagées sous forme de lien :

filters
search
pagination

Règle à retenir : l’état doit être stocké dans le plus petit contexte pertinent.

Application concrète sur une interface d’investissement

Imaginons une interface d’investissement en React Native qui a besoin de données de marché, d’un symbole sélectionné, de données de graphique, de données de portefeuille, d’un gestionnaire d’AppState, du défilement ainsi que d’une transformation coûteuse de graphiques. Une structure simplifiée :

InvestmentScreen
        │
        ├── useMarketData()
        │
        ├── usePortfolio()
        │
        ├── useAppState()
        │
        ├── useRef()
        │
        ├── useMemo()
        │
        └── useCallback()
                │
                ▼
              UI

Voici un aperçu du composant. Deux points à noter : le fragment se termine par quelques lignes de prose et des séparateurs qui ne constituent pas du code valide, il faut donc le considérer comme un pseudocode ; de plus, setSymbol provenant de useState est déjà stable, ce qui rend l’encapsulation avec useCallback optionnelle, permettant ainsi de passer directement le setter.

function InvestmentScreen() {
  const [symbol, setSymbol] = useState("AAPL");

const scrollRef = useRef<ScrollView>(null);
  const { data: marketData } = useMarketData(symbol);
  const { portfolio } = usePortfolio();
  const chartData = useMemo(() => {
    return transformMarketData(marketData);
  }, [marketData]);
  const handleSymbolChange = useCallback(
    (nextSymbol: string) => {
      setSymbol(nextSymbol);
    },
    []
  );
  return (
    <ScrollView ref={scrollRef}>
      <MarketHeader
        symbol={symbol}
        onSymbolChange={handleSymbolChange}
      />
      <PriceChart data={chartData} />
      <PortfolioSummary
        portfolio={portfolio}
      />
    </ScrollView>
  );
}

---

Now each concept has a clear responsibility.

---

Chaque Hook possède désormais une seule responsabilité clairement définie :

useState
→ selected symboluseRef
→ imperative ScrollView accessuseMemo
→ expensive chart transformationuseCallback
→ stable handler where usefulCustom Hooks
→ market and portfolio behaviouruseEffect
→ internal synchronization within those Hooks

Les effets restent à l’intérieur des Hooks personnalisés, où ils s’ synchronisent avec la source de données du marché et le cycle de vie de l’application, et l’écran lui-même ressemble presque à une description de ses données.

Répondre en tant qu’ingénieur senior

Aller au-delà de la question posée

Une erreur fréquente consiste à ne répondre qu’à ce qui a été demandé. Pour la question « Qu’est-ce que useMemo ? », répondre « Cela mémorise une valeur » est correct mais peu informatif. Une réponse plus complète :

useMemo cache une valeur calculée en se basant sur ses dépendances. Ce n’est pas une fonction par défaut, car elle entraîne des coûts supplémentaires et ajoute de la complexité. Elle s’avère utile lorsque des calculs coûteux sont répétés inutilement ou lorsqu’une référence stable est importante, de préférence après que l’analyse des performances ait identifié le goulot d’étranglement.

En quelques phrases, vous avez montré une connaissance de l’API, une conscience des performances, ainsi que la capacité d’évaluer les compromis et de prendre des décisions.

Une structure de réponse en cinq étapes

Un modèle fiable pour toute question concernant les Hooks :

1. What is it?
2. Why does it exist?
3. Real example
4. Trade-off / pitfall
5. React Native example when relevant

Appliqué à useRef :

  • Quoi : une valeur mutable persistante qui ne provoque pas de rendu.
  • Pourquoi : pour des données qui doivent persister entre les rendus mais ne font pas partie de l’interface utilisateur.
  • Exemple : un identifiant de timer ou une référence vers un composant impératif.
  • Compromis : comme sa mise à jour ne provoque pas de rendu, elle ne doit pas contenir d’état de l’interface utilisateur.
  • React Native : un ref permet un contrôle impératif sur un ScrollView.
  • Points à préparer pour les échanges suivants

    Pour useState, soyez prêt à discuter des fonctions de mise à jour et du regroupement de plusieurs appels, du fait que modifier une variable ordinaire ne modifie pas l’écran, de ce qui déclenche un re-render, du lieu où un état donné doit être stocké, et de la manière d’éviter les états qui sont en réalité dérivés.

    Pour useEffect, attendez-vous à des questions sur le rôle du tableau de dépendances, les closures obsolètes, les exécutions répétées, à quoi sert la fonction de nettoyage, comment annuler des tâches asynchrones en cours, les situations où useEffect n’est pas l’outil adapté, ainsi que sur la double invocation en mode Strict Mode pendant le développement.

    Pour useRef, préparez-vous à expliquer pourquoi écrire dans .current ne provoque pas de re-render, quels sont les cas où un ref est préférable à un état, comment les refs s’attachent aux éléments DOM ou aux vues natives, et ce qu’une API impérative signifie.

    Pour useMemo, il est essentiel de comprendre s’il accélère toujours les performances, en quoi il diffère de useCallback, ce que signifie l’égalité référentielle, quand il est opportun de l’enlever, et comment démontrer qu’il a été utile.

    Pour useCallback, il faut savoir pourquoi l’identité d’une fonction peut être importante, comment elle s’associe à React.memo, quand elle ne apporte aucun avantage, si elle peut ralentir les choses, et quels éléments doivent figurer dans sa liste de dépendances.

    Un exercice de débogage

    Les intervieweurs aiment tester des composants défectueux. Voici un exemple présentant plusieurs problèmes.

    function UserList({ users }) {
      const [search, setSearch] = useState("");
    
    const filteredUsers = useMemo(() => {
        return users.filter(user =>
          user.name.includes(search)
        );
      }, [users]);
      useEffect(() => {
        console.log("Search:", search);
      }, []);
      return (
        <List
          data={filteredUsers}
          renderItem={({ item }) => (
            <UserRow
              user={item}
              onPress={() => selectUser(item.id)}
            />
          )}
        />
      );
    }
    

    Dépendance manquante dans useMemo

    Le mémoisation est basé uniquement sur les utilisateurs :

    }, [users]);
    

    mais le filtre lit également

    search
    

    ce qui empêche la liste de s’actualiser pendant que l’utilisateur tape. Les dépendances doivent inclure les deux éléments :

    }, [users, search]);
    

    La règle de lintifierait cela immédiatement.

    Un effet figé sur la première valeur

    Cet effet dispose d’un tableau vide :

    useEffect(() => {
      console.log(search);
    }, []);
    

    il s’exécute donc uniquement après le montage et enregistre la première recherche search. Si l’objectif est d’enregistrer chaque modification, il faut s’y reposer :

    useEffect(() => {
      console.log(search);
    }, [search]);
    

    La fonction de rappel en ligne, gérée avec prudence

    Le gestionnaire de ligne

    onPress={() => selectUser(item.id)}
    

    est une nouvelle fonction à chaque rendu. Cela seul n’est pas un bug. Il faut d’abord déterminer si

    UserRow
    

    est coûteux et s’il provoque réellement un rendu inutile. Ce n’est qu’avec ces éléments de preuve que l’on devrait envisager

    useCallback
    

    ou restructurer les composants, par exemple en transmettant l’id et un gestionnaire stable à la ligne. Montrer cette retenue fait partie de la réponse attendue.

    Bonnes, solides et expertes réponses

    La progression est facile à mémoriser. Un bon développeur connaît useCallback. Un développeur compétent sait quand l’utiliser. Un développeur senior sait quand ne pas l’utiliser et peut expliquer les compromis associés. La même logique s’applique à chaque Hook. L’objectif n’est jamais

    Maximum Hooks
    

    mais plutôt

    Correct abstraction
    +
    Correct data flow
    +
    Correct performance trade-offs
    

    Un seul ensemble de concepts pour le web et les applications natives

    Vous n’avez pas besoin de connaissances distinctes pour les Hooks dans React et React Native. Sur le web, les exemples concernent généralement

    DOM
    browser events
    Web APIs
    WebSockets
    browser storage
    

    alors que les discussions sur les appareils mobiles portent sur

    AppState
    Keyboard
    native modules
    device APIs
    push notifications
    network state
    native event subscriptions
    

    Pourtant, l’outil de base reste le même dans les deux cas : React.

    useState
    useEffect
    useRef
    useMemo
    useCallback
    useContext
    useReducer
    Custom Hooks
    closures
    dependency arrays
    

    Expliquez donc d’abord le concept React, puis montrez comment l’appliquer sur la plateforme concernée. Cela vous permettra d’être crédible tant pour les rôles liés au web, aux mobiles qu’aux applications hybrides.

    Fiche de référence rapide

    useState

    Indique ce qui déclenche une réaffichage lors d’une mise à jour. Utilisations typiques :

    form values
    selected tabs
    modal visibility
    loading state
    local UI state
    

    useEffect

    Synchronisation avec des systèmes externes. Utilisations typiques :

    subscriptions
    timers
    WebSockets
    AppState
    native events
    network operations
    

    Évitez-le pour les valeurs que vous pouvez obtenir pendant le réaffichage.

    useRef

    Une valeur mutable persistante qui n’est pas réaffichée. Utilisations typiques :

    DOM/native refs
    timers
    previous values
    imperative APIs
    mutable non-UI data
    

    useMemo

    Une valeur calculée mémorisée. Utilisez-la lorsque

    calculation is expensive
    or
    referential identity matters
    

    et mesurez d’abord.

    useCallback

    Une référence à une fonction mémorisée. C’est utile lorsque

    function identity matters
    React.memo is involved
    another Hook depends on function identity
    

    Ne l’ajoutez pas automatiquement.

    useContext

    Lit des données contextuelles partagées, telles que :

    theme
    locale
    authentication
    configuration
    

    useReducer

    Modélise des transitions complexes. Utile lorsque vous avez

    many related state variables
    complex transitions
    state machine-like behaviour
    

    Custom Hooks

    Un comportement à état réutilisable, par exemple :

    useAuth()
    useNetworkStatus()
    useAppState()
    usePagination()
    useKeyboard()
    useUserProfile()
    

    Un modèle mental plutôt qu’un glossaire

    Au lieu de mémoriser une traduction d’un seul mot pour chaque Hook,

    useState = state
    useEffect = side effect
    useRef = reference
    useMemo = memoisation
    useCallback = callback
    

    imaginez comment ils interagissent avec le rendu : l’état de l’interface utilisateur circule via useState pour provoquer des rérendus, les valeurs dérivées proviennent de useMemo, les données modifiables non liées à l’interface se trouvent dans useRef, et useEffect relie l’interface rendue aux API, aux événements et aux systèmes natifs, les Hooks personnalisés regroupant tout cela.

    React Render
                         │
              ┌──────────┼──────────┐
              │          │          │
           UI State   Derived     Mutable
              │        Values       Data
              │          │          │
          useState    useMemo     useRef
              │
              ▼
           Re-render
              │
              ▼
              UI
              │
              ▼
       External Systems
              │
              ▼
          useEffect
              │
              ▼
     APIs / Events / Native
              │
              ▼
        Custom Hooks
    

    En dessous de tout cela se trouvent quelques principes fondamentaux :

    Rendering
    +
    Closures
    +
    Referential Equality
    +
    Data Flow
    +
    Performance
    

    Lorsque ces principes sont bien maîtrisés, les Hooks individuels deviennent plus simples à comprendre.

    Questions à poser lors de chaque revue de code

    Les Hooks sont des outils permettant de gérer différents types de comportements dans une application réactive, et la compétence essentielle est de savoir pourquoi on utilise chacun d’eux. Ces questions sont utiles tant lors des revues que pendant les entretiens :

    • Cet état est-il vraiment nécessaire, ou peut-il être dérivé ?
  • Cet effet est-il réellement synchronisé avec un élément externe, et ses dépendances sont-elles correctes ?
  • Ce callback pourrait-il lire une clôture obsolète ?
  • Cette référence contient-elle des données impératives, ou un état d’interface qui devrait être géré par useState ?
  • Cette mémoisation résout-elle un problème réellement mesuré ?
  • Cet Hook personnalisé cache-t-il une complexité utile, ou ajoute-t-il simplement de l’indirection ?
  • Cet état devrait-il être local ou global ?
  • Où se trouve le véritable goulot d’étranglement en termes de performance ?
  • En résumé

    Les Hooks deviennent plus simples dès que l’on cesse de les considérer comme des API distinctes et qu’on les voit plutôt comme des étapes d’un même flux :

    Rendering
       ↓
    State
       ↓
    Closures
       ↓
    Effects
       ↓
    References
       ↓
    Memoisation
       ↓
    Custom Hooks
       ↓
    Performance
       ↓
    Architecture
    

    Avec cette logique en tête, passer de React web à React Native devient bien moins intimidant. Lorsqu’on vous demande pourquoi vous avez utilisé useMemo à un endroit donné, évitez la définition classique et racontez plutôt l’histoire : le problème que vous avez rencontré, sa cause, pourquoi ce Hook convenait, les compromis que vous avez pris en compte, et comment vous avez évalué le résultat.

    Au préalable d’un entretien, assurez-vous de pouvoir parler naturellement des sujets suivants :

    • Les fondamentaux : les causes d’un rendu, ce qu’est un snapshot de rendu, la différence entre l’état et une simple variable, les closures, l’égalité référentielle et la réconciliation.
  • Hooks : les cinq courants utilisés au quotidien ainsi que useContext et useReducer, les hooks spécialisés (useLayoutEffect, useId, useImperativeHandle, useSyncExternalStore, useTransition, useDeferredValue) et la manière de concevoir des hooks personnalisés.
  • Effets : dépendances précises, valeurs obsolètes dans les fonctions de rappel, nettoyage et annulation, gestion des abonnements, comportement du mode strict et façon de déterminer quand aucun effet n’est nécessaire.
  • Prestations : manière dont React.memo compare les props, le coût de la mémorisation, l’analyse des rendus, les listes longues, les calculs intensifs et les abonnements excessivement larges au stockage.
  • React Native : le cycle de vie de l’application via AppState, la gestion du clavier, les modules natifs et les API du dispositif, les écouteurs enregistrés du côté natif, l’optimisation des listes, la séparation entre le travail en JavaScript et celui en natif, ainsi que le mode de rendu de la plateforme.
  • Architecture : état local versus état partagé, données du client versus données du serveur, où placer les limites des hooks personnalisés, de quoi est responsable chaque composant, qui possède chaque valeur, et la conception d’API de composants réutilisables.
  • L’objectif n’est pas de mémoriser des réponses pour tout, mais d’avoir une compréhension suffisante pour résoudre un problème inconnu face à l’intervieweur.