Fermer

janvier 20, 2022

Construire la vraie application avec React Query


Résumé rapide ↬

Dans cet article, Georgii Perepecho explique les fonctionnalités les plus courantes de React Query que vous devez connaître lors de la création d'une application réelle stable lors des tests.

Si vous avez déjà créé des applications React qui utilisent des données asynchrones, vous savez probablement à quel point il peut être ennuyeux de gérer différents états (chargement, erreur, etc.), de partager l'état entre les composants utilisant le même point de terminaison d'API et de conserver le état synchronisé dans vos composants.

Afin d'actualiser les données, nous devons faire beaucoup d'actions : définir les hooks useState et useEffectrécupérer les données de l'API, mettre les mises à jour données à l'état, modifier l'état de chargement, gérer les erreurs, etc. Heureusement, nous avons React Query, c'est-à-dire une bibliothèque qui facilite la récupération, la mise en cache et la gestion des données. ]déduplication de plusieurs requêtes pour les mêmes données en une seule requête ;

  • mise à jour des données "obsolètes" en arrière-plan (sur le focus Windows, la reconnexion, l'intervalle, etc.) ;
  • optimisations des performances telles que la pagination et le chargement différé données ;
  • mémorisation des résultats de la requête ;
  • prélecture des données ;
  • mutations, qui facilitent l'implémentation de changements optimistes.
  • Pour démontrer ces fonctionnalités, j'ai implémenté un exemple d'application, où j'ai essayé de couvrir tous les cas pour ceux que vous souhaitez utiliser React Query. L'application est écrite en TypeScript et utilise l'ARC, la requête React, le serveur factice Axios et l'interface utilisateur matérielle pour faciliter le prototypage. Il devrait pouvoir :

    • se connecter à l'aide d'une adresse e-mail et d'un mot de passe et indiquer l'utilisateur connecté ;
    • afficher la liste des prochains rendez-vous avec une fonctionnalité de chargement supplémentaire ;
    • afficher des informations sur un rendez-vous particulier ;
    • enregistrer et afficher l'historique des modifications ;
    • pré-récupérer des informations supplémentaires ;
    • ajouter et modifier les tâches requises.

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

    Interaction côté client

    Comme nous n'ont pas de vrai serveur principal, nous utiliserons axios-mock-adapter. J'ai préparé une sorte d'API REST avec des points de terminaison get/post/patch/delete. Pour stocker les données, nous utiliserons des fixtures. Rien de spécial – juste des variables que nous allons muter. nous sommes prêts à configurer React Query. C'est assez simple.

    Tout d'abord, nous devons envelopper notre application avec le fournisseur :

    const queryClient = new QueryClient();
    
    ReactDOM.render(
     
       
         
           
           
         
       
     ,
     document.getElementById('root')
    );

    Dans QueryClient()nous pourrions spécifier certaines valeurs globales par défaut.

    Pour un développement plus facile, nous allons créer nos propres abstractions pour les hooks React Query. Pour pouvoir souscrire à une requête, nous devons passer une clé unique. La façon la plus simple d'utiliser des chaînes, mais il est possible d'utiliser des clés de type tableau.

    Dans la documentation officielle, ils utilisent des clés de chaîne, mais je l'ai trouvé un peu redondant car nous avons déjà des URL pour appeler les demandes d'API. Ainsi, nous pourrions utiliser l'URL comme clé, de sorte que nous n'ayons pas besoin de créer de nouvelles chaînes pour les clés.

    Cependant, il existe certaines restrictions : si vous allez utiliser différentes URL pour GET/PATCHpar exemple, vous devez utiliser la même clé, sinon React Query ne pourra pas faire correspondre ces requêtes.

    De plus, nous devons garder à l'esprit qu'il est important d'inclure non seulement l'URL, mais également tous les paramètres que nous allons utiliser pour faire des requêtes au backend. Une combinaison d'URL et de paramètres créera une clé solide que la requête React utilisera pour la mise en cache. exporter const useFetch =(
    URL : chaîne | nul,
    paramètres ? : objet,
    config ? : UseQueryOptions
    ) => {
    contexte const = useQuery(
    [url!, params],
    ({ queryKey }) => récupération({ queryKey }),
    {
    activé : !!url,
    …configurer,
    }
    );

    contexte de retour ;
    } ;

    exporter l'extracteur const =({
    queryKey,
    pageParam,
    } : QueryFunctionContext) : Promesse => {
    const [url, params] = queryKey ;
    API de retour
    .get(url, { paramètres : { …params, pageParam } })
    .then((res) => res.data);
    };

    [url!, params] est notre clé, paramètre enabled : !!url que nous utilisons pour suspendre les requêtes s'il n'y a pas de clé (j'en reparlerai un peu plus tard). Pour l'extraction, nous pourrions utiliser n'importe quoi – cela n'a pas d'importance. Dans ce cas, j'ai choisi Axios.

    Pour une expérience de développement plus fluide, il est possible d'utiliser React Query Devtools en l'ajoutant au composant racine.

    import { ReactQueryDevtools } from 'react-query/devtools' ;
    
    ReactDOM.render(
     
       
         
           
           
           
         
       
     ,
     document.getElementById('root')
    );

    Outils de développement React Query

    Outils de développement React Query. ( Grand aperçu )

    Bien !

    Authentification

    Pour pouvoir utiliser notre application, nous devons nous connecter en saisissant l'e-mail et le mot de passe. Le serveur renvoie le jeton et nous le stockons dans des cookies (dans l'exemple d'application, toute combinaison d'e-mail/mot de passe fonctionne). Lorsqu'un utilisateur parcourt notre application, nous attachons le jeton à chaque demande.

    De plus, nous récupérons le profil de l'utilisateur par le jeton. Sur l'en-tête, nous affichons le nom d'utilisateur ou le chargement si la requête est toujours en cours. La partie intéressante est que nous pouvons gérer une redirection vers la page de connexion dans le composant racine Appmais afficher le nom d'utilisateur dans le composant séparé.

    C'est là que la magie React Query commence. En utilisant des hooks, nous pourrions facilement partager des données sur un utilisateur sans les transmettre en tant qu'accessoires.

    useEffet(() => {
    si (erreur) {
    history.replace(pageRoutes.auth);
    }
    }, [error]);

    UserProfile.tsx :

    const UserProfile = ({} : Props) => {
     const { données : utilisateur, isLoading } = useGetProfile();
    
     si (estChargement) {
       retourner (
         
           
         
       );
     }
    
     retourner (
       
         {utilisateur ? `Utilisateur : ${user.name}` : 'Non autorisé'}
       
     );
    } ;

    Et la requête à l'API ne sera appelée qu'une seule fois (cela s'appelle des requêtes de déduplication, et j'en parlerai un peu plus dans la section suivante).

    Hook pour récupérer les données de profil :[19659034]export const useGetProfile = () => {
    contexte const = useFetch<{ user: ProfileInterface }>(
    apiRoutes.getProfile,
    indéfini,
    { réessayer : faux }
    );
    return { …contexte, données : context.data?.user } ;
    };

    Nous utilisons ici le paramètre retry : false car nous ne voulons pas réessayer cette requête. En cas d'échec, nous pensons que l'utilisateur n'est pas autorisé et effectuons la redirection.

    Lorsque les utilisateurs saisissent leur identifiant et leur mot de passe, nous envoyons une requête POST régulière. Théoriquement, nous pourrions utiliser les mutations React Query ici, mais dans ce cas, nous n'avons pas besoin de spécifier const [btnLoading, setBtnLoading] = useState(false); l'état et le gérer, mais je pense que ce ne serait pas clair et probablement trop compliqué dans ce cas particulier.

    Si la requête aboutit, nous invalidons toutes les requêtes pour obtenir de nouvelles données. Dans notre application, il ne s'agirait que d'une requête : le profil de l'utilisateur pour mettre à jour le nom dans l'en-tête, mais juste pour être sûr de tout invalider.

    if (resp.data.token) {
     Cookies.set('token', resp.data.token);
     history.replace(pageRoutes.main);
     queryClient.invalidateQueries();
    }

    Si nous voulions invalider une seule requête, nous utiliserions queryClient.invalidateQueries(apiRoutes.getProfile);.

    Une page de connexion et un chargement de nom d'utilisateur.

    Plus À propos des requêtes de déduplication

    Supposons que nous ayons deux composants différents (ou même identiques) sur la page qui utilisent le même point de terminaison d'API. Habituellement, nous devrions faire deux demandes, qui sont en fait les mêmes, et c'est juste un gaspillage des ressources du backend. En utilisant React Query, nous pourrions dédupliquer les appels d'API avec les mêmes paramètres. Cela signifie que si nous avons des composants qui appellent les mêmes demandes, la demande ne sera effectuée qu'une seule fois.

    Dans notre application, nous avons deux composants : afficher le nombre total de rendez-vous et la liste.

    Composant total des rendez-vous :

     const UsersSummary = () => {
     const { données : liste, isLoading } = useGetAppointmentsList();
    
     si (!isLoading && !liste) {
       renvoie nul ;
     }
    
     retourner (
       
         
           
             
               Nombre total de rendez-vous :{' '}
               {est en cours de chargement ? (
                 
               ) : (
                 liste!.pages[0].count
               )}
             
           
         
       
     );
    } ;

    Composant de la liste des utilisateurs :

    const UsersList = () => {
     const {
       données : liste,
       est en cours de chargement,
       récupérerPageSuivante,
       aPageSuivante,
       isFetchingPageSuivante,
     } = useGetAppointmentsList();
    
     retourner (
       <>
         
           {est en cours de chargement ? (
             
               
                 
               
               
                 
               
               
                 
               
             
           ) : (
             
               {list!.pages.map((page) => (
                 
                   {page.data.map((item) => (
                     
                   ))}
                 
               ))}
             
           )}
         
         {aPageSuivante && (
           
             
           
         )}
       
     );
    };

    Ils utilisent le même crochet useGetAppointmentsList() où nous envoyons la requête à l'API. Comme nous avons pu le voir, la requête GET /api/getUserList n'a été appelée qu'une seule fois.

    Dédoublonnage des requêtes pour le même point de terminaison.

    Load More List

    Dans notre application, nous avons une infinité liste avec un bouton Charger plus. Nous ne pouvons pas implémenter cela en utilisant un crochet useQuery régulier, c'est pourquoi nous avons le crochet useInfiniteQueryil permet de gérer la pagination, en utilisant la fonction fetchNextPage.

     export const useGetAppointmentsList = () =>
     useLoadMore(apiRoutes.getUserList);

    Nous avons notre propre abstraction pour le hook React Query :

    export const useLoadMore = (url : string | null, params ? : object) => {
     contexte const = useInfiniteQuery<
       GetInfinitePagesInterface,
       Erreur,
       GetInfinitePagesInterface,
       QueryKeyT
     >(
       [url!, params],
       ({ queryKey, pageParam = 1 }) => fetcher({ queryKey, pageParam }),
       {
         getPreviousPageParam : (firstPage) => firstPage.previousId ?? faux,
         getNextPageParam : (dernièrePage) => {
           retourner lastPage.nextId ?? faux;
         },
       }
     );
    
     contexte de retour ;
    };

    C'est à peu près la même chose que nous avons pour le crochet useFetchici nous spécifions les fonctions getPreviousPageParam et getNextPageParambasées sur la réponse de l'API, et nous transmettons également la propriété pageParam à la fonction de récupération.

    const UsersList = () => {
     const {
       données : liste,
       est en cours de chargement,
       récupérerPageSuivante,
       aPageSuivante,
       isFetchingPageSuivante,
     } = useGetAppointmentsList();
    
     retourner (
       <>
         
           {est en cours de chargement ? (
             
               
                 
               
               
                 
               
               
                 
               
             
           ) : (
             
               {list!.pages.map((page) => (
                 
                   {page.data.map((item) => (
                     
                   ))}
                 
               ))}
             
           )}
         
         {aPageSuivante && (
           
             
           
         )}
       
     );
    };

    useInfiniteQuery hook a plusieurs champs supplémentaires comme fetchNextPagehasNextPageisFetchingNextPageque nous pourrions utiliser pour gérer notre charge plus de liste. Et les méthodes fetchNextPagefetchPreviousPage.

    A load more list.

    Indicateur de récupération d'arrière-plan/Refetching

    L'une des fonctionnalités les plus intéressantes pour moi est la récupération des données si nous changer le focus de la fenêtre, comme passer d'un onglet à l'autre. Par exemple, il pourrait être utile que les données puissent être modifiées par plusieurs auteurs. Dans ce cas, si nous gardons l'onglet du navigateur ouvert, nous n'avons pas à recharger la page. Nous verrons les données réelles lorsque nous nous concentrerons sur la fenêtre. De plus, nous pourrions utiliser un drapeau pour indiquer que la récupération est en cours. React Query a plusieurs paramètres au cas où vous n'en auriez pas besoin :

    • refetchInterval,
    • refetchIntervalInBackground,
    • refetchOnMount

      ,0 refetchOnReconnect,

    • refetchOnWindowFocus.

    Il est également possible de désactiver/activer les options globalement :

    const queryClient = new QueryClient({
     options par défaut : {
       requêtes : {
         refetchOnWindowFocus : faux,
       },
     },
    });

    Pour indiquer une nouvelle récupération, nous avons l'indicateur isFetching pour afficher l'état de chargement :

    Récupération en arrière-plan après avoir changé d'onglet.

    Faire des requêtes conditionnelles

    Comme nous utilisons des crochets pour récupérer les données, il pourrait être déroutant de savoir comment éviter de faire des demandes. Comme vous le savez, nous ne pouvons pas utiliser d'instructions conditionnelles avec des crochets, par exemple nous ne pouvons pas coder comme ça :

    if (data?.hasInsurance) {
     const { données : assurance } = useGetInsurance (
       data?.hasInsurance ? +id : nul
     );
    }

    Supposons que dans notre application, nous devions faire une demande supplémentaire pour obtenir les détails de l'assurance, en fonction de la réponse du point de terminaison du rendez-vous.

    Si nous voulons faire une demande, nous transmettons une clé, sinon nous transmettons null.[19659087]const { data: assurance } = useGetInsurance(data?.hasInsurance ? +id : null);

    export const useGetInsurance = (id: nombre | null) =>
    utiliserRécupérer(
    identifiant ? pathToUrl(apiRoutes.getInsurance, { id }) : null
    );

    Dans notre abstraction useFetchnous avons défini la propriété enabled dans la configuration sur false au cas où nous n'aurions pas de clé. Dans ce cas, React Query interrompt simplement les demandes.

    Pour un rendez-vous avec id = 1nous avons hasInsurance = true. Ensuite, nous faisons une autre demande et affichons une icône de vérification à côté du nom. Cela signifie que nous avons reçu un indicateur allCovered du point de terminaison getInsurance.

    La demande a été effectuée.

    La demande a été effectuée. ( Grand aperçu )

    Pour un rendez-vous avec id = 2 nous avons hasInsurance = falseet nous ne demandons pas les détails de l'assurance.

    La demande n'a pas été faite.

    La demande n'a pas été faite. ( Grand aperçu )

    Mutation simple avec invalidation des données

    Pour créer/mettre à jour/supprimer des données dans React Query, nous utilisons des mutations. Cela signifie que nous envoyons une requête au serveur, recevons une réponse et, sur la base d'une fonction de mise à jour définie, nous modifions notre état et le gardons à jour sans faire de requête supplémentaire.

    Nous avons une abstraction génétique pour ces actions.

     const useGenericMutation = (
     func : (données : S) => Promesse<AxiosResponse>,
     URL : chaîne,
     paramètres ? : objet,
     updater ? : ((oldData : T, newData : S) => T) | indéfini
    ) => {
     const queryClient = useQueryClient();
    
     return useMutation(fonc, {
       onMutate : asynchrone (données) => {
         attendre queryClient.cancelQueries([url!, params]);
    
         const previousData = queryClient.getQueryData([url!, params]);
    
    queryClient.setQueryData([url!, params](oldData) => {
     renvoyer le programme de mise à jour ? (oldData!, données) : données ;
    });
    
    
         retourner les données précédentes ;
       },
       // Si la mutation échoue, utilisez le contexte renvoyé par onMutate pour revenir en arrière
       onError : (erreur, _, contexte) => {
         queryClient.setQueryData([url!, params]contexte);
       },
    
       onSettled : () => {
         queryClient.invalidateQueries([url!, params]);
       },
     });
    } ;

    Regardons plus en détail. Nous avons plusieurs méthodes de callback :

    onMutate (si la requête aboutit) :

    1. Annulez toutes les requêtes en cours.
    2. Enregistrer les données actuelles dans une variable.[19659007]Si défini, nous utilisons une fonction updater pour faire muter notre état par une logique spécifique, sinon, remplacez simplement l'état par les nouvelles données. Dans la plupart des cas, il est logique de définir la fonction updater.
    3. Renvoyer les données précédentes.

    onError (si la requête a échoué) :

    1. Annuler les données précédentes. .

    onSettled (si la requête réussit ou échoue) :

    1. Invalidez la requête pour conserver l'état frais.

    Cette abstraction que nous utiliserons pour toutes les actions de mutation.

    export const useDelete = (
     URL : chaîne,
     paramètres ? : objet,
     updater ? : (oldData : T, id : chaîne | nombre) => T
    ) => {
     return useGenericMutation(
       (id) => api.delete(`${url}/${id}`),
       URL,
       paramètres,
       mise à jour
     );
    } ;
    
    export const usePost = (
     URL : chaîne,
     paramètres ? : objet,
     updater ? : (oldData : T, newData : S) => T
    ) => {
     return useGenericMutation(
       (données) => api.post(url, données),
       URL,
       paramètres,
       mise à jour
     );
    } ;
    
    export const useUpdate = (
     URL : chaîne,
     paramètres ? : objet,
     updater ? : (oldData : T, newData : S) => T
    ) => {
     return useGenericMutation(
       (données) => api.patch(url, données),
       URL,
       paramètres,
       mise à jour
     );
    };

    C'est pourquoi il est très important d'avoir le même ensemble de [url!, params] (que nous utilisons comme clé) dans tous les crochets. Sans cela, la bibliothèque ne pourra pas invalider l'état et faire correspondre les requêtes.

    Voyons comment cela fonctionne dans notre application : nous avons une section Historiqueen cliquant sur Enregistrer bouton, nous envoyons une requête PATCH et recevons l'ensemble de l'objet de rendez-vous mis à jour.

    Tout d'abord, nous définissons une mutation. Pour l'instant, nous n'allons pas effectuer de logique complexe, renvoyant simplement le nouvel état, c'est pourquoi nous ne spécifions pas la fonction updater.

    const mutation = usePatchAppointment(+id);
    
    export const usePatchAppointment = (id: nombre) =>
     utiliserMise à jour(
       pathToUrl(apiRoutes.rendez-vous, { id })
     );

    Remarque : Il utilise notre crochet générique useUpdate.

    Enfin, nous appelons la méthode mutate avec les données que nous voulons corriger : mutation.mutate([data!]);.

    Mutation de données simple avec invalidation.

    Remarque : dans ce composant, nous utilisons un isFetching pour indiquer la mise à jour des données sur le focus de la fenêtre (consultez la section de récupération Background), ainsi, nous affichons l'état de chargement à chaque fois que la requête est en cours. C'est pourquoi, lorsque nous cliquons sur Enregistrermodifions l'état et récupérons la réponse réelle, nous affichons également l'état de chargement. Idéalement, cela ne devrait pas être affiché dans ce cas, mais je n'ai pas trouvé de moyen d'indiquer une récupération en arrière-plan, mais n'indiquez pas la récupération lors du chargement des nouvelles données.

    const History = ({ id } : Props ) => {
     const { données, isFetching } = useGetAppointment(+id);
     const mutation = usePatchAppointment(+id);
    
     si (estFeching) {
       retourner (
         
           
             
           
           
             
           
           
             
           
         
       );
     }
    
     const onSubmit = () => {
       mutation.mutate(data!);
     } ;
    
     retourner (
       <>
         {data?.history.map((item) => (
           
             Date : {article.date} 
    Commentaire : {item.comment}
    ))} {!data?.history.length && !isFetching && ( Rien trouvé )} ); } ;

    Mutation avec changements optimistes

    Regardons maintenant l'exemple le plus complexe : dans notre application, nous voulons avoir une liste, où nous devrions pouvoir ajouter et supprimer des éléments. De plus, nous voulons rendre l'expérience utilisateur aussi fluide que possible. Nous allons implémenter des changements optimistes pour la création/la suppression d'emplois.

    Voici les actions :

    1. L'utilisateur saisit le nom de l'emploi et clique sur le bouton Ajouter.
    2. Nous ajoutons immédiatement cet élément à notre liste et affichez le chargeur sur le bouton Ajouter.
    3. En parallèle, nous envoyons une requête à l'API.
    4. Lorsque la réponse est reçue, nous masquons le chargeur, et s'il réussit, nous gardons simplement le entrée précédente, mettez à jour son identifiant dans la liste et effacez le champ de saisie.
    5. Si la réponse échoue, nous affichons la notification d'erreur, supprimons cet élément de la liste et conservons le champ de saisie avec l'ancienne valeur.
    6. Dans les deux cas, nous envoyons une requête GET à l'API pour nous assurer que nous avons l'état réel.

    Toute notre logique est la suivante :

    const { data, isLoading } = useGetJobs();
    
    const mutationAdd = useAddJob((oldData, newData) => [...oldData, newData]);
    const mutationDelete = useDeleteJob((oldData, id) =>
     oldData.filter((item) => item.id !== id)
    );
    
    const onAdd = asynchrone () => {
     essayer {
       attendre mutationAdd.mutateAsync({
         nom : nom du travail,
         identifiant de rendez-vous,
       });
       setNomJob('');
     } attrape (e) {
       pushNotification(`Impossible d'ajouter le travail : ${jobName}`);
     }
    } ;
    
    const onDelete = async (id : numéro) => {
     essayer {
       attendre mutationDelete.mutateAsync(id);
     } attrape (e) {
       pushNotification(`Impossible de supprimer le travail`);
     }
    };

    Dans cet exemple, nous définissons nos propres fonctions updater pour faire muter l'état par une logique personnalisée : pour nous, il s'agit simplement de créer un tableau avec le nouvel élément et de filtrer par id si nous voulons supprimer l'élément. Mais la logique peut être quelconque, cela dépend de vos tâches.

    React Query s'occupe de changer d'état, de faire des requêtes et de revenir à l'état précédent en cas de problème.

    Dans la console, vous pouvez voir quelles requêtes axios fait à notre API fictive. Nous avons pu voir immédiatement la liste mise à jour dans l'interface utilisateur, puis nous avons appelé POST et enfin nous avons appelé GET. Cela fonctionne parce que nous avons défini le rappel onSettled dans le hook useGenericMutationdonc après un succès ou une erreur, nous récupérons toujours les données :

    onSettled : () => {
     queryClient.invalidateQueries([url!, params]);
    },
    Modifications optimistes avec requêtes en arrière-plan et invalidation des données.

    Remarque : Lorsque je surligne les lignes dans les outils de développement, vous pouvez voir un grand nombre de requêtes effectuées. En effet, nous modifions le focus de la fenêtre lorsque nous cliquons sur la fenêtre Outils de développement et React Query invalide l'état.

    Si le backend renvoyait l'erreur, nous annulerions les modifications optimistes et afficherions la notification. Cela fonctionne parce que nous avons défini le rappel onError dans le hook useGenericMutation. Nous définissons donc les données précédentes si une erreur s'est produite :

    onError : (err, _, context) => {
     queryClient.setQueryData([url!, params]contexte);
    },
    Modifications optimistes et restauration des données en raison de l'erreur.

    Préchargement

    La prélecture peut être utile si nous voulons disposer des données à l'avance et s'il existe une forte probabilité qu'un utilisateur demande ces données dans un futur proche.

    Dans notre exemple, nous allons pré-extraire les détails de la voiture si l'utilisateur déplace le curseur de la souris dans la zone de section Additional.

    Pré-extraire les données si un utilisateur déplace le curseur dans la zone zone.

    Lorsque l'utilisateur clique sur le bouton Affichernous rendons les données immédiatement, sans appeler l'API (malgré un délai d'une seconde).

    const prefetchCarDetails = usePrefetchCarDetails(+id) ;
    
    onMouseEnter={() => {
     si (!prefetched.current) {
       prefetchCarDetails();
       prefetched.current = vrai;
     }
    }}
    
    export const usePrefetchCarDetails = (id: nombre | null) =>
     usePrefetch(
       identifiant ? pathToUrl(apiRoutes.getCarDetail, { id }) : null
     );

    Nous avons notre hook d'abstraction pour la prélecture :

    export const usePrefetch = (url : string | null, params ? : object) => {
     const queryClient = useQueryClient();
    
     retour () => {
       si (!url) {
         retourner;
       }
    
       queryClient.prefetchQuery(
         [url!, params],
         ({ queryKey }) => récupération({ queryKey })
       );
     } ;
    } ;

    Pour rendre les détails de la voiture, nous utilisons le composant CarDetailsoù nous définissons un crochet pour récupérer les données.

    const CarDetails = ({ id } : Props) => {
     const { données, isLoading } = useGetCarDetail(id);
    
     si (estChargement) {
       retour  ;
     }
    
     si (!données) {
       retour Rien trouvé ;
     }
    
     retourner (
       
         
           Modèle : {data.model}
         
    
         
           Numéro : {data.number}
         
       
     ) ;
    } ;
    
    export const useGetCarDetail = (id: nombre | null) =>
     utiliserRécupérer(
       pathToUrl(apiRoutes.getCarDetail, { id }),
       indéfini,
       { staleTime : 2000 }
     );

    Bon point que nous n'avons pas à transmettre d'accessoires supplémentaires à ce composant, donc dans le composant Appointment nous prélevons les données et dans le composant CarDetails nous utilisons utilisez le crochet GetCarDetail pour récupérer les données prérécupérées.

    En définissant un staleTime étendu, nous permettons aux utilisateurs de passer un peu plus de temps avant de cliquer sur le bouton Afficher. Sans ce paramètre, la requête pourrait être appelée deux fois s'il faut trop de temps entre le déplacement du curseur sur la zone de préchargement et le clic sur le bouton.

    Suspense

    Suspense est une fonctionnalité expérimentale de React qui permet d'attendre du code. de manière déclarative. En d'autres termes, nous pourrions appeler le composant Suspense et définir le composant fallbackque nous voulons afficher pendant que nous attendons les données. Nous n'avons même pas besoin du drapeau isLoading de React Query. Pour plus d'informations, veuillez vous référer à la documentation officielle.

    Supposons que nous ayons une liste de Services et que nous voulions afficher l'erreur, et Réessayez bouton si quelque chose s'est mal passé.

    Pour obtenir la nouvelle expérience de développeur, utilisons ensemble Suspense, React Query et Error Boundaries. Pour le dernier, nous utiliserons react-error-boundary
    Bibliothèque.

    
     {({ réinitialiser }) => (
        (
           
             
               
                 Erreur !
               
               {error.message}
             
    
             
               
             
           
         )}
         onReset={réinitialiser}
       >
         <Réagir.Suspense
           repli={
             
               
                 
               
               
                 
               
               
                 
               
             
           }
         >
           
         
       
     )}
    

    Dans le composant Suspense, nous rendons notre composant ServiceCheckoù nous appelons le point de terminaison API pour la liste de services.

    const { data } = useGetServices();

    In le crochet, nous définissons suspense : true et retry : 0.

    export const useGetServices = () =>
     useFetch(apiRoutes.getServices, non défini, {
       suspens : vrai,
       réessayer : 0,
     });

    Sur le serveur fictif, nous envoyons une réponse de 200 ou 500 codes d'état au hasard.

    mock.onGet(apiRoutes.getServices).reply( (config) => {
     si (!getUser(config)) {
       retour [403] ;
     }
    
     const échoué = !!Math.round(Math.random());
    
     si (échec) {
       retour [500] ;
     }
    
     retour [200, services] ;
    });
    Gestion des erreurs à l'aide de React Query, Suspense et ErrorBoundary.

    Ainsi, si nous recevons une erreur de l'API et que nous ne la gérons pas, nous affichons la notification rouge avec le message du réponse. En cliquant sur le bouton Réessayernous appelons la méthode resetErrorBoundary()qui essaie d'appeler à nouveau la requête. Dans React Suspense fallback, nous avons notre composant squelette de chargement, qui s'affiche lorsque nous effectuons les requêtes. et ne devrait probablement pas être utilisé en production pour le moment. Nous allons utiliser React Testing Library et Jest.

    Tout d'abord, nous créons une abstraction pour les composants de rendu.

    export const renderComponent = (children : React.ReactElement, history : any) => {
     const QueryClient = new QueryClient({
       options par défaut : {
         requêtes : {
           réessayer : faux,
         },
       },
     });
     options constantes = rendu(
       
         {enfants}
       
      );
    
     retourner {
       ...options,
       déboguer : (
         el? : HTMLElement,
         maxLength = 300000,
         opt ? : joliFormat.OptionsReceived
       ) => options.debug(el, maxLength, opt),
     } ;
    } ;

    Nous avons défini retry : false comme paramètre par défaut dans QueryClient et encapsulons un composant avec QueryClientProvider.

    Maintenant, testons notre Composant de rendez-vous.

    Nous commençons par le plus simple : il suffit de vérifier que le composant s'affiche correctement.

    test('devrait rendre la page principale', async () => {
     const moqué = mockAxiosGetRequests({
       '/api/rendez-vous/1' : {
         identifiant : 1,
         nom : 'Hector McKeown',
         date_rendez-vous : '2021-08-25T17:52:48.132Z',
         services : [1, 2],
         adresse : 'Londres',
         véhicule : 'FR14ERF',
         commentaire : 'La voiture ne fonctionne pas correctement',
         histoire : [],
         hasInsurance : vrai,
       },
       '/api/travail' : [],
       '/api/getServices' : [
         {
           id: 1,
           name: 'Replace a cambelt',
         },
         {
           id: 2,
           name: 'Replace oil and filter',
         },
         {
           id: 3,
           name: 'Replace front brake pads and discs',
         },
         {
           id: 4,
           name: 'Replace rare brake pads and discs',
         },
       ],
       '/api/getInsurance/1' : {
         allCovered : vrai,
       },
     });
     const history = createMemoryHistory();
     const { getByText, queryByTestId } = renderComponent(
       ,
       l'histoire
     );
    
     expect(queryByTestId('rendement-squelette')).toBeInTheDocument();
    
     attendre waitFor(() => {
       expect(queryByTestId('appointment-skeleton')).not.toBeInTheDocument();
     });
    
     expect(getByText('Hector Mckeown')).toBeInTheDocument();
     expect(getByText('Replace a cambelt')).toBeInTheDocument();
     expect(getByText('Replace oil and filter')).toBeInTheDocument();
     expect(getByText('Remplacer les plaquettes et disques de frein avant')).toBeInTheDocument();
     expect(queryByTestId('DoneAllIcon')).toBeInTheDocument();
     attendre(
       mocked.mock.calls.some((item) => item[0] === '/api/getInsurance/1')
     ).toBeTruthy();
    });

    Nous avons préparé des assistants pour simuler les requêtes Axios. Dans les tests, nous avons pu spécifier l'URL et les données fictives.

    const getMockedData = (
     URL d'origine : chaîne,
     mockData : { [url: string] : tout },
     type : chaîne
    ) => {
     const foundUrl = Object.keys(mockData).find((url) =>
       originalUrl.match(new RegExp(`${url}$`))
     );
    
     si (!urltrouvée) {
       return Promise.reject(
         new Error(`Called unmocked api ${type} ${originalUrl}`)
       );
     }
    
     si (erreur mockData[foundUrl] instanceof) {
       return Promise.reject(mockData[foundUrl]);
     }
    
     return Promise.resolve({ data: mockData[foundUrl] });
    } ;
    
    export const mockAxiosGetRequests = (mockData : {
    
    }) : fonction fictive => {
     // @ts-ignore
     return axios.get.mockImplementation((originalUrl) =>
       getMockedData(originalUrl, mockData, 'GET')
     );
    };

    Ensuite, nous vérifions qu'il y a un état de chargement et ensuite, attendons le démontage du composant de chargement.

    expect(queryByTestId('appointment-skeleton')).toBeInTheDocument();
    
     attendre waitFor(() => {
       expect(queryByTestId('appointment-skeleton')).not.toBeInTheDocument();
     });

    Ensuite, nous vérifions qu'il y a des textes nécessaires dans le composant rendu, et enfin vérifions que la requête API pour les détails de l'assurance a été appelée.

    attendez-vous(
       mocked.mock.calls.some((item) => item[0] === '/api/getInsurance/1')
     ).toBeTruthy();

    Il vérifie que le chargement des indicateurs, la récupération des données et l'appel des points de terminaison fonctionnent correctement.

    Dans le texte suivant, nous vérifions que nous n'appelons pas la demande de détails d'assurance si nous n'en avons pas besoin (remember in the component we have a condition, that if in the response from appointment endpoint there is a flag hasInsurance: true we should call the insurance endpoint, otherwise we shouldn't).

    test( 'should not call and render Insurance flag', async () => {
     const mocked = mockAxiosGetRequests({
       '/api/appointment/1': {
         id: 1,
         name: 'Hector Mckeown',
         appointment_date: '2021-08-25T17:52:48.132Z',
         services: [1, 2],
         address: 'London',
         vehicle: 'FR14ERF',
         comment: 'Car does not work correctly',
         history: [],
         hasInsurance: false,
       },
       '/api/getServices': [],
       '/api/job': [],
     });
     const history = createMemoryHistory();
     const { queryByTestId } = renderComponent(history);
    
     await waitFor(() => {
       expect(queryByTestId('appointment-skeleton')).not.toBeInTheDocument();
     });
    
     expect(queryByTestId('DoneAllIcon')).not.toBeInTheDocument();
    
     expect(
       mocked.mock.calls.some((item) => item[0] === '/api/getInsurance/1')
     ).toBeFalsy();
    });

    This test checks that if we have hasInsurance: false in the response, we will not call the insurance endpoint and render the icon.

    Last, we are going to test mutations in our Jobs component. The whole test case:

    test('should be able to add and remove elements', async () => {
     const mockedPost = mockAxiosPostRequests({
       '/api/job': {
         name: 'First item',
         appointmentId: 1,
       },
     });
    
     const mockedDelete = mockAxiosDeleteRequests({
       '/api/job/1': {},
     });
    
     const history = createMemoryHistory();
     const { queryByTestId, queryByText } = renderComponent(
       ,
       history
     );
    
     await waitFor(() => {
       expect(queryByTestId('loading-skeleton')).not.toBeInTheDocument();
     });
    
     await changeTextFieldByTestId('input', 'First item');
    
     await clickByTestId('add');
    
     mockAxiosGetRequests({
       '/api/job': [
         {
           id: 1,
           name: 'First item',
           appointmentId: 1,
         },
       ],
     });
    
     await waitFor(() => {
       expect(queryByText('First item')).toBeInTheDocument();
     });
    
     expect(
       mockedPost.mock.calls.some((item) => item[0] === '/api/job')
     ).toBeTruthy();
    
     await clickByTestId('delete-1');
    
     mockAxiosGetRequests({
       '/api/job': [],
     });
    
     await waitFor(() => {
       expect(queryByText('First item')).not.toBeInTheDocument();
     });
    
     expect(
       mockedDelete.mock.calls.some((item) => item[0] === '/api/job/1')
     ).toBeTruthy();
    });

    Let’s see what is happening here.

    1. We mock requests for POST and DELETE.
    2. Input some text in the field and press the button.
    3. Mock GET endpoint again, because we assume that POST request has been made, and the real server should send us the updated data; in our case, it’s a list with 1 item.
    4. Wait for the updated text in the rendered component.
    5. Check that the POST request to api/job has been called.
    6. Click the Delete button.
    7. Mock GET endpoint again with an empty list (like in the previous case we assume the server sent us the updated data after deleting).
    8. Check that deleted item doesn’t exist in the document.
    9. Check that the DELETE request to api/job/1 has been called.

    Important Note: We need to clear all mocks after each test to avoid mixing them up.

    afterEach(() => {
     jest.clearAllMocks();
    });

    Conclusion

    With the help of this real-life application, we went through all of the most common React Query features: how to fetch data, manage states, share between components, make it easier to implement optimistic changes and infinite lists, and learned how to make the app stable with tests.

    I hope I could interest you in trying out this new approach in your current or upcoming projects.

    Resources

    Smashing Editorial" width="35" height="46" loading="lazy" decoding="async(vf, yk, il)




    Source link

    Revenir vers le haut