Construire la vraie application avec React Query

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 useEffect
ré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 ;
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.
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/PATCH
par 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
const [url, params] = queryKey ;
API de retour
.get
.then((res) => res.data);
};
Où [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')
);
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 App
mais 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);
.
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.
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 useInfiniteQuery
il 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 useFetch
ici nous spécifions les fonctions getPreviousPageParam
et getNextPageParam
basé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 fetchNextPage
hasNextPage
isFetchingNextPage
que nous pourrions utiliser pour gérer notre charge plus de liste. Et les méthodes fetchNextPage
fetchPreviousPage
.
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 :
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 useFetch
nous 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 = 1
nous 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
.
Pour un rendez-vous avec id = 2
nous avons hasInsurance = false
et nous ne demandons pas les détails de l'assurance.
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) :
- Annulez toutes les requêtes en cours.
- 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 fonctionupdater
. - Renvoyer les données précédentes.
onError
(si la requête a échoué) :
- Annuler les données précédentes. .
onSettled
(si la requête réussit ou échoue) :
- 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 Historique
en 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!]);
.
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 Enregistrer
modifions 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 :
- L'utilisateur saisit le nom de l'emploi et clique sur le bouton
Ajouter
. - Nous ajoutons immédiatement cet élément à notre liste et affichez le chargeur sur le bouton
Ajouter
. - En parallèle, nous envoyons une requête à l'API.
- 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.
- 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.
- 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 useGenericMutation
donc après un succès ou une erreur, nous récupérons toujours les données :
onSettled : () => {
queryClient.invalidateQueries([url!, params]);
},
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);
},
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
.
Lorsque l'utilisateur clique sur le bouton Afficher
nous 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 CarDetails
où 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 fallback
que 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 ServiceCheck
où 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] ;
});
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éessayer
nous 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.
- We mock requests for
POST
andDELETE
. - Input some text in the field and press the button.
- Mock
GET
endpoint again, because we assume thatPOST
request has been made, and the real server should send us the updated data; in our case, it’s a list with 1 item. - Wait for the updated text in the rendered component.
- Check that the
POST
request toapi/job
has been called. - Click the
Delete
button. - Mock
GET
endpoint again with an empty list (like in the previous case we assume the server sent us the updated data after deleting). - Check that deleted item doesn’t exist in the document.
- Check that the
DELETE
request toapi/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

Source link