React Query et son implémentation chez Neomanis

React Query_article 2

Article rédigé par

Raphaël_Avatar
Raphaël Huiban

Développeur JavaScript

Qu'est-ce que React Query et comment est-ce implémenté chez Neomanis ?

Chez Neomanis, nous avions commencé à développer le front-end de l’application sans utiliser de librairie externe pour gérer nos différentes requêtes d’API. Cela marchait bien au début, car les fonctionnalités de l’application restaient simples.

Par exemple, voici à quoi pouvait ressembler un composant simple contenant une requête :

function UserList() { 
    const [users, setUsers] = useState<User[]>(); 
    const [isLoading, setIsLoading] = useState(true); 
    const [isError, setIsError] = useState(false); 
 
    useEffect(() => { 
        const fetchData = async () => { 
            try { 
                const data = await getUsers(); 
                setUsers(data); 
                setIsLoading(false); 
            } catch (error) { 
                setIsError(true); 
            } 
        }; 
 
        fetchData(); 
    }, []); 
 
    if (isLoading) { 
        return <Loading />; 
    } 
    if (isError) { 
        return <Error />; 
    } 
 
    return ( 
        <ul> 
            {users.map((user) => ( 
                <li key={user.id}>{user.name}</li> 
            ))} 
        </ul> 
    ); 
} 

C’est un cas d’utilisation pourtant très simple, mais qui nécessite quand même trois states pour la gestion de la donnée, du loader et de l’erreur. Souvent, on peut ajouter le fait de devoir gérer d’autres requêtes, une ou plusieurs logiques internes au composant, etc.

En plus de cela, au fur et à mesure de la conception et du développement, nous devions ajouter de nouvelles fonctionnalités, un système de cache, une synchronisation des données via des événements websocket, une parallélisation des requêtes, une meilleure gestion des erreurs, … Tout cela allait complexifier encore plus le code et donc en réduire la lisibilité.

J’ai donc décidé de partir à la recherche de différentes solutions pour améliorer plus facilement l’expérience utilisateur et des meilleures conditions de développement.

Une première solution aurait été de refactoriser toutes nos implémentations de récupération de données en un hook custom le plus générique possible et d’essayer de faire la même chose pour l’update et la création.

Mais en cherchant des exemples de bonnes pratiques concernant un custom hook dans le genre, je suis tombé sur des discussions et vidéos qui parlaient de deux librairies : React Query et SWR. En voyant l’entrain que la communauté de développeurs React avait pour ces deux librairies, je me suis donc empressé de les tester. Après plusieurs tests, j’ai préféré les fonctionnalités que React Query avait à proposer. J’ai donc fait une présentation à l’équipe et nous avons opté pour refactoriser nos requêtes d’API en utilisant React Query.

1 – Mais concrètement, c’est quoi React Query ?

React Query est une librairie qui met à disposition plusieurs custom hook permettant de mieux organiser vos requêtes API, avoir un système de cache déjà embarqué et réglable, synchroniser vos states serveur de n’importe quel endroit de l’application, et plein d’autres que je vous laisse découvrir dans leur documentation.

Je ne vais pas faire un tutoriel complet de React Query. Pour ça, je vous laisse parcourir leur documentation ou chercher les nombreux tutoriels/présentations de la librairie qui ont déjà été faits sur le web.

2 - Comment est utilisé React Query chez Neomanis ?

Les options par défaut

Après avoir beaucoup joué avec les différents hooks que proposent React Query, nous sommes arrivés à une implémentation plutôt stable. Dans un premier temps, nous allons parler du staleTime et du cacheTime. Par défaut, React Query propose un staleTime de 0, ce qui signifie qu’un refetch des données se fera automatiquement sur un remount du useQuery en question, sur un refocus de la fenêtre ou si le réseau s’est reconnecté.

Chez Neomanis, on se base sur un système de websocket pour notifier à un utilisateur que ces données ne sont pas à jour et qu’il a besoin de refetch certaines données. On a donc établi un staleTime et cacheTime plus élevé pour prévenir des refetch trop fréquents.

Notre implémentation de useQuery

Pour une meilleure lisibilité du code, nous avons décidé de ne passer que par des hook custom contenant les différent hook de React Query et les différentes logiques dont on a besoin.

Par exemple voilà à quoi va ressembler un hook d’une requête d’API qui va récupérer la liste des tickets liée à un utilisateur :

export function useTicketsRelativeToUser(userUid: string | undefined) { 
    return useQuery(["tickets", userUid], () => getTicketsRelativeToUser(userUid), { 
        enabled: !!userUid, 
    }); 
} 

Comme l’uid de l’utilisateur ne sera pas toujours définie, nous utilisons l’option enabled de useQuery pour faire en sorte de récupérer la donnée seulement lorsque l’uid de l’utilisateur est définie.

De temps en temps, nous utilisons aussi l’option select de useQuery qui permet de changer la forme de la donnée que l’on récupère.

Par exemple, ici, on récupère une liste de catégories que l’on formate pour pouvoir directement l’utiliser dans l’un de nos inputs de type select :

export function useCategories() { 
    return useQuery("categories", () => getGlpiCategories(), { 
        select: (data) => data.map((category) => ({ label: category.name, value: category.id })), 
    }); 
} 

Notre système de websocket

Comme dit plus haut, nous utilisons un système de websocket qui permet de notifier l’utilisateur si une de ses données est périmée (stale).

Par exemple, nous avons un cas où lorsqu’un autre technicien fait des modifications sur un ticket qui est lié à plusieurs utilisateurs, les autres utilisateurs recevront une notification pour signaler la présence de modifications et le passer à l’état stale. Donc la prochaine fois que l’utilisateur ira sur l’écran du ticket en question, un refetch sera fait pour récupérer les nouvelles données à jour.

    const queryClient = useQueryClient(); 
 
    useEffect(() => { 
        socket.on(NotificationEvent.TICKET_UPDATE, (data) => { 
            queryClient.invalidateQueries(["ticket", data.objectId]); 
        }); 
 
        return () => { 
            socket.off(NotificationEvent.TICKET_UPDATE); 
        }; 
    }, []); 

Cela nous permet d’avoir des données afficher à l’utilisateur qui sont le plus à jour possible tout en effectuant le moins de requêtes d’API possible !

Et pour la création et la mise à jour ?

Et bien c’est pareil, nous avons créé des hook custom pour gérer chaque mutation.

Voici un exemple de création de réponse à une question technique :

export function useCreateAnswer() { 
    const queryClient = useQueryClient(); 
    const { toasterDispatch } = useContext(GlobalContext); 
    const { t } = useTranslation(); 
 
    return useMutation( 
        ({ text, user, technicalQuestionId }: CreateAnswer) => 
            createAnswerForOneTechnicalQuestion(technicalQuestionId, { text, user }), 
        { 
            onSuccess: (result, { technicalQuestionId }) => { 
                queryClient.setQueryData<TechnicalQuestionAnswer[]>(["answers", technicalQuestionId], (oldAnswers) => [ 
                    result, 
                    ...(oldAnswers ?? []), 
                ]); 
            }, 
            onError: () => { 
                toasterDispatch({ 
                    type: "SIMPLE_MESSAGE", 
                    payload: { 
                        position: "top", 
                        toasterMessage: t("error.somethingWentWrong"), 
                        callBackEnd: () => toasterDispatch({ type: "RESET" }), 
                        emotion: "sad", 
                    }, 
                }); 
            }, 
        } 
    ); 
} 

On peut voir qu’on utilise l’option onSuccess pour directement mettre à jour la donnée des réponses, on ajoute la nouvelle réponse au tableau de réponse déjà existant.

En cas d’erreur, nous avons un système de toaster qui est géré par un useReducer dans le contexte de l’application, qui va nous permettre d’afficher un message d’erreur temporaire à l’utilisateur.

3 - Conclusion

React Query répond donc à notre problématique d’origine : Améliorer l’expérience utilisateur avec des fonctionnalités facile d’accès comme la gestion d’erreur, ou la synchronisation des données.

Par la même occasion son implémentation a permis d’avoir une meilleure expérience de développement grâce à un code plus lisible et le fait qu’il est très facile de sortir des composants toutes les logiques liées aux requêtes d’API.

J’espère que ce retour d’expériences vous auras permis d’avoir un petit tour d’horizons des différentes possibilités qu’offre React Query, de notre côté nous allons continuer à suivre de très près les prochaines évolutions de cette bibliothèque !

4 - Ressources

Articles de blog similaires

Suivez-nous sur les réseaux sociaux

0 réflexion sur “React Query et son implémentation chez Neomanis”

  1. article difficile à suivre à cause des formulations assez mauvaise, exemple :
    – « C’est un cas d’utilisation pourtant très simple, mais qui nécessite quand même trois states pour la gestion de la donnée, du loader et de l’erreur »

    1. Raphaël Huiban

      Bonjour, merci pour votre avis.
      La phrase que vous citez explique que, pour un cas d’utilisation simple, et que l’on peut retrouver dans n’importe quelle application, il y a déjà une base de code complexe. Lorsqu’il y aura de nouvelles fonctionnalités à ajouter autour de ce code, la tâche sera de plus en plus difficile et le code en sera de moins en moins lisible.

      J’espère avoir répondu à votre questionnement et peut-être auriez-vous plus de précisions concernant les autres problèmes que vous auriez rencontrés ?

      Raphaël

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.