- Published on
Comment fonctionne useQuery et useMutation dans react-query et comment l'utiliser ?
- Authors
- Name
- Léo Delpon
Comment fonctionnent concrètement useQuery et useMutation dans react-query et comment l’utiliser ?
Je sais, je suis en retard, mais mieux vaut tard que jamais ! Dans ce tutoriel, je vous propose de découvrir une librairie à 34K étoiles
, celle de Query
créé par TanStack
(on va beaucoup plus s’attarder sur react-query). A quoi ca va nous servir ? Ca nous permettre de gérer plusieurs choses :
- La récupération des données.
- La mise en cache.
- La synchronisation.
- La mise à jour des données du serveur.
Enfin, on considère useQuery
comme étant déclaratif
. Je veux dire par là que les requêtes s'exécutent principalement de manière automatique
. Vous définissez les dépendances, mais React Query se charge d'exécuter la requête immédiatement, et effectue également des mises à jour intelligentes en arrière-plan lorsque cela est jugé nécessaire. Cela fonctionne très bien pour les requêtes, car nous voulons que ce que nous voyons à l'écran soit synchronisé avec les données réelles du backend.
Vous allez me dire, “Mais attend, ca ressemble vachement à ce que Redux fait non ?”. Alors oui et non, car react-query
et Redux
ont des objectifs différents mais les problèmes qu’ils résoudent se rejoignent. Par exemple, Redux
se concentre sur la gestion de l'état complet de l'application, y compris l'état local et l'état du serveur, en utilisant un seul store global, des actions et des réducteurs. Il est basé sur l'architecture Flux
et impose un flux de données unidirectionnel
, ce qui facilite le raisonnement sur les changements d'état complexes
. React Query, quant à lui, est spécifiquement conçu pour les applications React et se concentre sur la récupération, la mise en cache et la gestion des données du serveur. Il ne s'agit pas d'une bibliothèque de gestion d'état polyvalente comme Redux, et elle ne gère pas l'état local de l'application. React Query gère automatiquement
la mise en cache, la récupération en arrière-plan et la gestion des erreurs, qui sont souvent mises en œuvre manuellement ou avec des middlewares supplémentaires dans Redux.
En fonction des besoins de votre application, vous pouvez utiliser uniquement React Query, uniquement Redux ou une combinaison des deux. Si vous je ne vous ai tout du moins pas convaincu, je vous invite à aller lire cette partie de la documentation !
Rappel des bases
Pour commencer à pouvoir l’utiliser, nous allons “câbler” notre “Provider” comme ceci :
//pages/_app.tsx (Next.js project)
import { QueryClient, QueryClientProvider } from 'react-query'
const twentyFourHoursInMs = 1000 * 60 * 60 * 24
// Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
retry: false,
staleTime: twentyFourHoursInMs,
},
},
})
// Provide the QueryClientProvider to access queryClient through all components in our project
function MyApp({ Component, pageProps }: AppProps) {
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
)
}
export default MyApp
Ceci paraît un peu obscur, je vais donc vous expliquer à quoi correspondent ces options dans queryClient
:
refetchOnWindowFocus
: Cela permet de ne pas recharger automatiquement les données lorsqu’une fenêtre ou un onglet obtient un focus différent de ce que l’on avaitrefetchOnMount
: Cela ne rechargera pas automatiquement les données lorsqu'un composant utilisant une requête est monté.refetchOnReconnect
: Cela ne rechargera pas automatiquement les données lorsqu'une connexion réseau est rétablie après une déconnexion.retry
: Cela n'essaiera pas de réessayer automatiquement une requête échouée. A savoir que par défaut, react-query effectuera 3 fois une requête si elle echoue et après c’est tout.staleTime
: Cette option détermine combien de temps (en millisecondes) les données récupérées peuvent être considérées comme "fraîches" avant de devenir "obsolètes”. J’ai mis à 1 jour la durée de validité (86 400 000 ms
pour les try hardeurs du calcul).
Implémentation de react-query sur une application qui récupère des annonces d’emploi (en utilisant l’API de remoteOk)
Je vais essayer de vous faire implémenter cette pratique en faisant quelque chose d’utile. Je suis personnellement en dernière année, donc prochainement disponible dans le marché du travail (d’ailleurs si en attendant vous avez une idée business et que cela nécessite des compétences en web, je suis votre homme de freelance 🙃 ), cela pourrait être cool de créer une mini dashboard qui centralise les offres de jobs ! Attention, je ne vais vous faire faire que la partie react-query
et non la partie UI/UX, pour ça, je vous laisserai faire. (quoi que ca sera peut être un sujet d’article qui sait ? 🤔).
Ok, maintenant que tout est configuré econcernant notre base de react-query
et que je vous ai contextualisé le mini-projet, on va pouvoir passer à l’étape suivante: celle des services
. Afin d’assurer une modularité un peu plus agréable. Je vous propose de créer carrément une structuration de service
comme ceci :
On crée notre petit helper
pour avoir une configuration modulable. Nous allons donc utiliser axios
pour avoir un client HTTP. Il est aussi possible d’utiliser fetch
mais je préfère personnellement axios
pour son côté isomorphique
. Vous pouvez voir que j’ai crée aussi
autres fonctions qui sont withAuthorization
, initializeJwtHeader
et eraseJwtHeader
. C’est pour une partie authentification si vous souhaitez accéder à un endpoint
qui est protégé. (il n’est pas obligatoire de le faire mais j’avais envie)
// secondary-adapters/service/helpers/api.helpers.ts
import axios from 'axios'
import Cookies from 'js-cookie'
const axiosConfig = axios.create({
baseURL: 'URL_DE_TON_API_OU_NIMPORTE_QUELLE_AUTRE_API',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Headers': '*',
},
})
export const withAuthorization = () => {
const token = Cookies.get('UN_JWT_AU_CAS_OU_T_ES_ENVIE_DE_SETUP_DE_L_AUTORISATION')
if (token !== undefined) {
axiosConfig.defaults.headers.common['Authorization'] = `Bearer ${token}`
}
}
export const initializeJwtHeader = (token: string) => {
Cookies.set('UN_JWT_AU_CAS_OU_T_ES_ENVIE_DE_SETUP_DE_L_AUTORISATION', token, {
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 3),
})
axiosConfig.defaults.headers.common['Authorization'] = `Bearer ${token}`
}
export const eraseJwtHeader = (token: string) => {
Cookies.remove(token)
}
export default axiosConfig
A noter que URL_DE_TON_API_OU_NIMPORTE_QUELLE_AUTRE_API
doit être l’url de votre API ou d’un microservice API externe. (Je ferai bientôt un article sur comment setup rapidement une API pour le mettre à dispositino d’un service front, vous pourrez ainsi l’utiliser pour “compléter” ce projet !)
On peut ensuite créer notre service avec notre modèle de donnée que l’on va récupérer depuis notre appel API
.
// types/index.ts
type DetectedExtension = {
posted_at: string,
schedule_type: string,
work_from_home: boolean,
}
type JobHighlight = {
title: string,
items: string[],
}
export type JobOffer = {
id: string,
title: string,
location: string,
company_name: string,
description: string,
thumbnail: string,
detected_extensions: DetectedExtension,
extensions: string[],
job_highlights: JobHighlight[],
}
Ce modèle de donnée varie en fonction de ce que vous souhaitez récupérer. En effet, je me suis basé sur les données que j’obtiens depuis l’api SerpAPI
. Je vous montrerai dans un prochain article comment je l’ai utilisé pour récolter les données des JobOffer
. Vous pouvez totalement utiliser une autre source de données comme celle-ci. Les données en rendus sont un peu dégueux, je vous propose de la parser avant via celui-ci ou un autre site
// secondary-adapters/service/remote-ok-job.service.ts
import axiosConfig from "../../helpers/api.helpers";
import { JobOffer } from "../../../types"
const getJobOffers = async (): Promise<JobOffer[]> => {
const response = await axiosConfig.get<JobOffer[]>("/remote-ok-job");
return response.data;
}
const RemoteOkService = {
getJobOffers
}
export default RemotOkService;
A présent, on va aller dans notre page web et utiliser l’ensemble des choses que nous avons configuré. Pour récupérer les données JSON depuis notre API, nous allons utiliser un hook personnalisé useQuery()
qui attend plusieurs paramètres:
- Une clé unique qui nous servira à identifier la requête parmis toutes les requêtes de l’application.
- Une fonction de récupération de données qui est pour nous
RemoteOkService.getJobOffers
. - Un objet d’options
// pages/index.tsx
import * as React from 'react';
import { useQuery } from 'react-query';
import RemoteOkService from '@/secondary-adapters/services/remote-ok/remote-ok.service';
export default function HomePage() {
const [search, setSearch] = React.useState("React developer")
// focus et ref ne sont pas encore utilisé mais ça va venir ne vous inquietez pas :)
const [focus, setFocus] = React.useState(false)
const refInput = React.useRef(null)
// ça non plus on l'utilise pas encore
const [jobOffers, setJobOffers] = React.useState<JobOffer[]>([])
const {data, isLoading, refetch, isSuccess, isError, error} = useQuery(["jobOffers"], async () => {
return await RemoteOkService.getJobOffers(search)
}, {enable: false, retry: 1, onSuccess: (res) => {console.log(res)}, onError: (err: any) => {console.log(err) }});
return (
<Layout>
{
isSuccess && data.map((element) => {
return (
<div>
{
element.company_name
}
</div>
)
})
}
</Layout>
);
}
Je vais vous expliquer un peu plus en détail ce que j’ai fais dans cette partie :
const { data, isLoading, refetch, isSuccess, isError, error } = useQuery(
['jobOffers'],
async () => {
return await RemoteOkService.getJobOffers(search)
},
{
enable: false,
retry: 1,
onSuccess: (res) => {
console.log(res)
},
onError: (err: any) => {
console.log(err)
},
}
)
Concernant les elements qui en retournent :
data
correspond aux données reçues.isLoading
est un boolean qui permet de savoir si des données on été stockées en cache et que la requête est en cours d’éxécution.refetch
permet de réexécuter manuellement la requête.isSuccess
est un boolean pour savoir si il n’y pas eu d’erreurs et que les données sont prêtes à être visualisées.isError
assez explicite.error
je pense que c’est encore plus explicite.
Maintenant, passons aux deux callbacks
:
onSuccess
: la fonction de rappel qui sera déclenchée chaque fois que la requête réussira à récupérer de nouvelles données.onError
: la fonction de rappel qui sera déclenchée si la requête rencontre une erreur et à laquelle sera transmise l'erreur
Point informatif concernant cette fameuse, clé. La requête sera automatiquement mise à jour lorsque cette clé changera (dans le cas où enabled
n'est pas défini sur false). Si vous souhaitez exécuter la requête à chaque fois que la recherche change, vous pouvez utiliser queryKey
comme ceci : ['query-tutorials', search].
const { data, isLoading, refetch, isSuccess, isError, error } = useQuery(
['jobOffers', search],
async () => {
return await RemoteOkService.getJobOffers(search)
},
{
enable: false,
retry: 1,
onSuccess: (res) => {
console.log(res)
},
onError: (err: any) => {
console.log(err)
},
}
)
Bien, à présent, nous avons notre méthode pour obtenir les données mais qu’en est-il de la modification ou encore un refetch
mais avec une recherche différente ?
Utilisation de useMutation()
useMutation
est un hook fourni par React Query pour effectuer des mutations, c'est-à-dire des actions qui modifient les données du serveur, comme la création, la mise à jour ou la suppression d'éléments. Contrairement à useQuery
, qui est destiné à récupérer des données, ainsi,useMutation
est conçu pour effectuer des opérations qui ont des effets secondaires sur les données du serveur. On dit que useMutation
est impératif
.
Pour ce faire, j’ai créé une nouvelle fonction pour réffectuer une nouvelle requête
const newSearch = useMutation(
async () => {
return await RemoteOkService.getJobOffers(search)
},
{
onSuccess: (res) => {
setJobOffers([...res])
},
}
)
Ainsi, le fameux search
va être modifié grâce à ce bout HTML que j’ai ajouté au dessus de notre génération dynamique d’offre. (Pour les plus agiles, vous avez pu voir que j’utilisais tailwindcss 😃)
<div>
<h2 className="font-lato">Trouve ton offre d'emploi ici !</h2>
<div className="py-4">
<div className={`grid grid-cols-12 bg-white py-2 px-2 rounded-lg w-1/2 ${focus ? 'border-red-500' : 'border-transparent'} border-2 transition duration-200`}>
<div className="col-span-10 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
<input
onChange={(event) => {setSearch(event.target.value)}}
value={search}
className="outline-none px-2 w-full font-medium"
placeholder="Titre du job ou mots clés"
ref={refInput}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
/>
</div>
<div className="col-span-2" onClick={(event) => {
event.preventDefault()
newSearch.mutate()
}}>
<button className="bg-[#FF3951] hover:bg-[#bb2738] transition duration-300 px-5 text-sm text-white py-2 rounded-lg font-medium w-full">
Chercher
</button>
</div>
</div>
</div>
</div>
</div>
Ainsi, grâce à ceci, nous pouvons donc modifier à chaud notre valeur search
et lorsque l’on clique sur le bouton, nous avons une nouvelle requête qui s’effectue ! Vous pourrez voir que l’on a bien accès aux nouvelles données, ce qui est plutôt cool n’est-ce pas ?
Bonus, faire de la pagination avec useQuery()
Pour permettre un affichage infinie, il est nécessaire de garder les données précédentes. Il est totalement possible de le faire via le comportement natif des clés. Et oui, la requête sera automatiquement mise à jour lorsque cette clé changera, ainsi on peut faire un truc comme ça
const [pageNumber, setPageNumber] = React.useState<number>(0)
const {data, isLoading, refetch, isSuccess, isError, error, isPreviousData} = useQuery(["jobOffers", pageNumber], async () => {
return await RemoteOkService.getJobOffers(search, pageNumber)
}, {keepPreviousData: true, retry: 1, onSuccess: (res) => {console.log(res)}, onError: (err: any) => {console.log(err) }});
return (
...
<button
onClick={() => setPageNumber(old => Math.max(old - 1, 0))}
disabled={pageNumber === 0}
>
Précédent
</button>{' '}
<button
onClick={() => {
if (!isPreviousData && data.hasMore) {
setPageNumber(old => old + 1)
}
}}
// Permet de désactiver le bouton
disabled={isPreviousData || !data?.hasMore}
>
Prochain
</button>
)
Et voilà, j’espère que cet article vous aura plu ! React Query
est une bibliothèque extrêmement utile pour simplifier le développement d'applications React en abordant spécifiquement les problèmes liés à la récupération, la gestion et la synchronisation des données du serveur. En fournissant une solution dédiée pour gérer l'état des données du serveur, React Query
permet aux développeurs de se concentrer sur la logique métier et l'expérience utilisateur sans se soucier des détails techniques de la gestion des données. Je vous ai montré une autre manière de gérer la donnée dans une application react, vous penserez peut-être moi la prochaine fois qu’on vous demandera de configurer la mastodonte Redux pour une vielle application qui ne nécessite pas une architecture de l’espace !
Petit aperçu du projet que je vais bientôt publier :
Aller, PEACE