Comment créer un crochet de réaction personnalisé pour récupérer et mettre en cache des données

Publié: 2022-03-10
Résumé rapide ↬ Il y a de fortes chances que de nombreux composants de votre application React aient à faire des appels à une API pour récupérer des données qui seront affichées à vos utilisateurs. Il est déjà possible de le faire en utilisant la méthode de cycle de vie componentDidMount() , mais avec l'introduction de Hooks, vous pouvez créer un hook personnalisé qui récupérera et mettra en cache les données pour vous. C'est ce que ce tutoriel couvrira.

Si vous êtes un novice de React Hooks, vous pouvez commencer par consulter la documentation officielle pour en avoir une idée. Après cela, je vous recommande de lire « Premiers pas avec l'API React Hooks » de Shedrack Akintayo. Pour vous assurer que vous suivez, il y a aussi un article écrit par Adeneye David Abiodun qui couvre les meilleures pratiques avec React Hooks qui, j'en suis sûr, s'avérera utile pour vous.

Tout au long de cet article, nous utiliserons l'API Hacker News Search pour créer un crochet personnalisé que nous pourrons utiliser pour récupérer des données. Bien que ce didacticiel couvre l'API Hacker News Search, nous ferons en sorte que le hook fonctionne de manière à ce qu'il renvoie une réponse à partir de tout lien API valide que nous lui transmettrons.

Meilleures pratiques de réaction

React est une fantastique bibliothèque JavaScript pour créer des interfaces utilisateur riches. Il fournit une excellente abstraction de composants pour organiser vos interfaces dans un code qui fonctionne bien, et vous pouvez l'utiliser pour à peu près tout. Lire un article connexe sur React →

Récupération de données dans un composant React

Avant les crochets React, il était conventionnel de récupérer les données initiales dans la méthode de cycle de vie componentDidMount() et les données basées sur les changements d'accessoires ou d'état dans la méthode de cycle de vie componentDidUpdate() .

Voici comment cela fonctionne:

 componentDidMount() { const fetchData = async () => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=JavaScript` ); const data = await response.json(); this.setState({ data }); }; fetchData(); } componentDidUpdate(previousProps, previousState) { if (previousState.query !== this.state.query) { const fetchData = async () => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${this.state.query}` ); const data = await response.json(); this.setState({ data }); }; fetchData(); } }

La méthode de cycle de vie componentDidMount est invoquée dès que le composant est monté, et lorsque cela est fait, nous avons fait une requête pour rechercher "JavaScript" via l'API Hacker News et mettre à jour l'état en fonction de la réponse.

La méthode de cycle de vie componentDidUpdate , d'autre part, est invoquée lorsqu'il y a un changement dans le composant. Nous avons comparé la requête précédente dans l'état avec la requête actuelle pour éviter que la méthode ne soit invoquée chaque fois que nous définissons « données » dans l'état. L'une des choses que nous obtenons de l'utilisation des crochets est de combiner les deux méthodes de cycle de vie de manière plus propre, ce qui signifie que nous n'aurons pas besoin d'avoir deux méthodes de cycle de vie pour le montage du composant et sa mise à jour.

Plus après saut! Continuez à lire ci-dessous ↓

Récupération de données avec useEffect Hook

Le crochet useEffect est appelé dès que le composant est monté. Si nous avons besoin que le crochet soit réexécuté en fonction de certains changements d'accessoires ou d'états, nous devrons les transmettre au tableau de dépendances (qui est le deuxième argument du crochet useEffect ).

Voyons comment récupérer des données avec des hooks :

 import { useState, useEffect } from 'react'; const [status, setStatus] = useState('idle'); const [query, setQuery] = useState(''); const [data, setData] = useState([]); useEffect(() => { if (!query) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${query}` ); const data = await response.json(); setData(data.hits); setStatus('fetched'); }; fetchData(); }, [query]);

Dans l'exemple ci-dessus, nous avons transmis query en tant que dépendance à notre crochet useEffect . En faisant cela, nous disons à useEffect de suivre les modifications de la requête. Si la valeur de la query précédente n'est pas la même que la valeur actuelle, useEffect est à nouveau invoqué.

Cela dit, nous définissons également plusieurs status sur le composant selon les besoins, car cela transmettra mieux un message à l'écran en fonction de certains status d'états finis. À l'état inactif , nous pourrions informer les utilisateurs qu'ils peuvent utiliser le champ de recherche pour commencer. Dans l'état de récupération , nous pourrions montrer un spinner . Et, dans l'état récupéré , nous rendrons les données.

Il est important de définir les données avant d'essayer de définir l'état sur fetched afin d'éviter un scintillement résultant du fait que les données sont vides pendant que vous définissez l'état fetched .

Création d'un crochet personnalisé

"Un hook personnalisé est une fonction JavaScript dont le nom commence par 'use' et qui peut appeler d'autres Hooks."

— Réagissez aux documents

C'est vraiment ce que c'est, et avec une fonction JavaScript, cela vous permet de réutiliser un morceau de code dans plusieurs parties de votre application.

La définition de React Docs l'a dévoilé mais voyons comment cela fonctionne en pratique avec un contre-hook personnalisé :

 const useCounter = (initialState = 0) => { const [count, setCount] = useState(initialState); const add = () => setCount(count + 1); const subtract = () => setCount(count - 1); return { count, add, subtract }; };

Ici, nous avons une fonction régulière dans laquelle nous prenons un argument facultatif, définissons la valeur sur notre état, et ajoutons les méthodes d' add et de subtract qui pourraient être utilisées pour le mettre à jour.

Partout dans notre application où nous avons besoin d'un compteur, nous pouvons appeler useCounter comme une fonction normale et passer un initialState afin que nous sachions d'où commencer à compter. Lorsque nous n'avons pas d'état initial, la valeur par défaut est 0.

Voici comment cela fonctionne en pratique :

 import { useCounter } from './customHookPath'; const { count, add, subtract } = useCounter(100); eventHandler(() => { add(); // or subtract(); });

Ce que nous avons fait ici a été d'importer notre crochet personnalisé à partir du fichier dans lequel nous l'avons déclaré, afin que nous puissions l'utiliser dans notre application. Nous définissons son état initial sur 100, donc chaque fois que nous appelons add() , il augmente count de 1, et chaque fois que nous appelons subtract() , il diminue count de 1.

Création d'un hook useFetch

Maintenant que nous avons appris à créer un crochet personnalisé simple, extrayons notre logique pour récupérer des données dans un crochet personnalisé.

 const useFetch = (query) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!query) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${query}` ); const data = await response.json(); setData(data.hits); setStatus('fetched'); }; fetchData(); }, [query]); return { status, data }; };

C'est à peu près la même chose que nous avons faite ci-dessus, à l'exception du fait qu'il s'agit d'une fonction qui prend en query et renvoie status et data . Et c'est un crochet useFetch que nous pourrions utiliser dans plusieurs composants de notre application React.

Cela fonctionne, mais le problème avec cette implémentation maintenant est qu'elle est spécifique à Hacker News, donc nous pourrions simplement l'appeler useHackerNews . Ce que nous avons l'intention de faire, c'est de créer un crochet useFetch qui peut être utilisé pour appeler n'importe quelle URL. Réorganisons-le pour prendre une URL à la place !

 const useFetch = (url) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch(url); const data = await response.json(); setData(data); setStatus('fetched'); }; fetchData(); }, [url]); return { status, data }; };

Maintenant, notre hook useFetch est générique et nous pouvons l'utiliser comme nous le voulons dans nos différents composants.

Voici une façon de le consommer :

 const [query, setQuery] = useState(''); const url = query && `https://hn.algolia.com/api/v1/search?query=${query}`; const { status, data } = useFetch(url);

Dans ce cas, si la valeur de query est truthy , nous allons de l'avant pour définir l'URL et si ce n'est pas le cas, nous sommes d'accord pour passer undefined car il serait géré dans notre crochet. L'effet tentera de s'exécuter une fois, quoi qu'il en soit.

Mémorisation des données récupérées

La mémorisation est une technique que nous utiliserions pour nous assurer que nous n'atteignons pas le point de terminaison hackernews si nous avons fait une sorte de demande pour le récupérer lors d'une phase initiale. Le stockage du résultat d'appels de récupération coûteux permettra aux utilisateurs de gagner du temps de chargement, augmentant ainsi les performances globales.

Note : Pour plus de contexte, vous pouvez consulter l'explication de Wikipedia sur la mémorisation.

Explorons comment nous pourrions faire cela !

 const cache = {}; const useFetch = (url) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); if (cache[url]) { const data = cache[url]; setData(data); setStatus('fetched'); } else { const response = await fetch(url); const data = await response.json(); cache[url] = data; // set response in cache; setData(data); setStatus('fetched'); } }; fetchData(); }, [url]); return { status, data }; };

Ici, nous mappons les URL à leurs données. Donc, si nous faisons une demande pour récupérer des données existantes, nous définissons les données à partir de notre cache local, sinon, nous procédons à la demande et définissons le résultat dans le cache. Cela garantit que nous ne faisons pas d'appel d'API lorsque nous avons les données à notre disposition localement. Nous remarquerons également que nous supprimons l'effet si l'URL est falsy , ce qui garantit que nous ne procédons pas à la récupération de données qui n'existent pas. Nous ne pouvons pas le faire avant le crochet useEffect car cela irait à l'encontre de l'une des règles des crochets, qui est de toujours appeler les crochets au niveau supérieur.

Déclarer le cache dans une portée différente fonctionne mais cela rend notre hook aller à l'encontre du principe d'une fonction pure. En outre, nous voulons également nous assurer que React aide à nettoyer notre gâchis lorsque nous ne voulons plus utiliser le composant. Nous allons explorer useRef pour nous aider à y parvenir.

Mémoriser des données avec useRef

" useRef est comme une boîte qui peut contenir une valeur mutable dans sa .current property ."

— Réagissez aux documents

Avec useRef , nous pouvons définir et récupérer facilement des valeurs modifiables et sa valeur persiste tout au long du cycle de vie du composant.

Remplaçons notre implémentation de cache par un peu de magie useRef !

 const useFetch = (url) => { const cache = useRef({}); const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); if (cache.current[url]) { const data = cache.current[url]; setData(data); setStatus('fetched'); } else { const response = await fetch(url); const data = await response.json(); cache.current[url] = data; // set response in cache; setData(data); setStatus('fetched'); } }; fetchData(); }, [url]); return { status, data }; };

Ici, notre cache est maintenant dans notre crochet useFetch avec un objet vide comme valeur initiale.

Emballer

Eh bien, j'ai déclaré que définir les données avant de définir le statut récupéré était une bonne idée, mais il y a aussi deux problèmes potentiels que nous pourrions avoir avec cela :

  1. Notre test unitaire peut échouer si le tableau de données n'est pas vide pendant que nous sommes dans l'état de récupération. React pourrait en fait grouper les changements d'état, mais il ne peut pas le faire s'il est déclenché de manière asynchrone ;
  2. Notre application restitue plus qu'elle ne le devrait.

Faisons un dernier nettoyage de notre crochet useFetch . Nous allons commencer par changer nos useState s en useReducer . Voyons comment cela fonctionne !

 const initialState = { status: 'idle', error: null, data: [], }; const [state, dispatch] = useReducer((state, action) => { switch (action.type) { case 'FETCHING': return { ...initialState, status: 'fetching' }; case 'FETCHED': return { ...initialState, status: 'fetched', data: action.payload }; case 'FETCH_ERROR': return { ...initialState, status: 'error', error: action.payload }; default: return state; } }, initialState);

Ici, nous avons ajouté un état initial qui est la valeur initiale que nous avons transmise à chacun de nos useState individuels. Dans notre useReducer , nous vérifions le type d'action que nous voulons effectuer et définissons les valeurs appropriées à déclarer en fonction de cela.

Cela résout les deux problèmes dont nous avons discuté précédemment, car nous pouvons maintenant définir le statut et les données en même temps afin d'aider à prévenir les états impossibles et les re-rendus inutiles.

Il ne reste plus qu'une chose : nettoyer nos effets secondaires. Fetch implémente l'API Promise, dans le sens où il pourrait être résolu ou rejeté. Si notre hook essaie de faire une mise à jour alors que le composant est démonté en raison d'une Promise qui vient d'être résolue, React renverra Can't perform a React state update on an unmounted component.

Voyons comment nous pouvons résoudre ce problème avec le nettoyage useEffect !

 useEffect(() => { let cancelRequest = false; if (!url) return; const fetchData = async () => { dispatch({ type: 'FETCHING' }); if (cache.current[url]) { const data = cache.current[url]; dispatch({ type: 'FETCHED', payload: data }); } else { try { const response = await fetch(url); const data = await response.json(); cache.current[url] = data; if (cancelRequest) return; dispatch({ type: 'FETCHED', payload: data }); } catch (error) { if (cancelRequest) return; dispatch({ type: 'FETCH_ERROR', payload: error.message }); } } }; fetchData(); return function cleanup() { cancelRequest = true; }; }, [url]);

Ici, nous définissons cancelRequest sur true après l'avoir défini dans l'effet. Ainsi, avant d'essayer de modifier l'état, nous confirmons d'abord si le composant a été démonté. S'il a été démonté, nous ignorons la mise à jour de l'état et s'il n'a pas été démonté, nous mettons à jour l'état. Cela résoudra l'erreur de mise à jour de l'état de réaction et empêchera également les conditions de concurrence dans nos composants.

Conclusion

Nous avons exploré plusieurs concepts de crochets pour aider à récupérer et à mettre en cache des données dans nos composants. Nous avons également nettoyé notre hook useEffect qui aide à prévenir un bon nombre de problèmes dans notre application.

Si vous avez des questions, n'hésitez pas à les déposer dans la section commentaires ci-dessous!

  • Voir le dépôt de cet article →

Les références

  • "Présentation des crochets", React Docs
  • "Démarrer avec l'API React Hooks", Shedrack Akintayo
  • "Meilleures pratiques avec React Hooks", Adeneye David Abiodun
  • « Programmation fonctionnelle : fonctions pures », Arne Brasseur