Alexandre BorelLead Frontend & Mobile Developer

Utiliser Tanstack Query de manière propre et efficace

Publié le 30/06/2025• Alexandre BOREL

Tanstack Query est aujourd’hui une des librairies que j’utilise dans toutes mes SPA, son utilisation est vraiment simple, et permet de réduire pas mal le code écrit. Mais comment structurer son code avec Tanstack Query de manière propre et efficace ?

L’utilisation des “queries” de base

Dans une utilisation basique nous allons avoir une query des plus simples, qui s’occupe simplement de faire notre appel.

const { data, isLoading, /* ... */ } = useQuery({
    queryKey: ['posts'],
    queryFn: async (): Promise<Array<Post>> => {
      const response = await fetch('https://jsonplaceholder.typicode.com/posts')
      return await response.json()
    },
  })

Il est possible d’utiliser directement ce useQuery dans nos composants frontend, mais un des soucis que l’on va rencontrer c’est lorsque l’application va commencer à grandir et que l’on va vouloir réutiliser cette query dans d’autres composants (sans avoir à dupliquer de code).

La “Query Factory” pour une structure scalable

La “Query Factory” va nous permettre d’isoler nos queries de nos composants tout en permettant de garder un maximum de souplesse lors de leur usage. On va voir avec des exemples que lorsque notre application grandit, on va être amené à devoir invalider du cache ou lancer des “refetch” de certaines requêtes. Rien de mieux qu’une structure de code adaptée pour cela.

Plusieurs itérations pour en arriver à la solution optimale

Avant d’en arriver à cette structure, j’ai tenté de mettre en place des systèmes de réutilisation des queries qui n’étaient pas optimales.

Ma première approche était de créer un fichier par query que je voulais réutiliser.

export default function usePostsQuery(queryOptions) {
   return useQuery({
    queryKey: ['posts'],
    queryFn: async (): Promise<Array<Post>> => {
      const response = await fetch('https://jsonplaceholder.typicode.com/posts')
      return await response.json()
    },
    ...queryOptions
  })
}

Sauf qu’en utilisant ce système il m’est arrivé souvent de batailler avec le typage Typescript et je me retrouvais avec beaucoup de fichiers.

Dans un second temps, nous avons essayé de commencer à faire une “query factory” à l’aide d’une énumération, ce qui me permettait de faire des invalidations de cache plus facilement, mais je devais modifier beaucoup plus de choses.

// src/lib/query-keys.ts
enum QueryKey {
  POST_DETAILS = 'postDetails',
  POST_LIST = 'postList',
}

// src/lib/queries.ts
export const postQuery = useQuery({
    queryKey: [QueryKey.POST_LIST],
    queryFn: async () => {
      const response = await fetch('https://jsonplaceholder.typicode.com/posts')
      return await response.json()
    },
  })

Cette solution nous contraignait à maintenir une énumération (qui est vite devenue énorme) qui devait être synchronisée avec nos queries. C’est devenu compliqué à maintenir (oubli de suppression de query keys, certaines queries utilisait la même query key, etc …).

On a donc décidé de prendre un peu plus de recul et de tenter d’utiliser un package fait par la communauté : https://github.com/lukemorales/query-key-factory.

En fin de compte, nous nous sommes rendus compte que faire notre propre query factory n’était pas si compliqué que ça à mettre en place, ne nécessitait aucun package supplémentaire, qu’il était adapté à tous les frameworks javascript supportés par “tanstack query” et que le code était très propre !

En quoi ça consiste techniquement

Nous allons définir nos queries par domaines ou modules (à vous de voir selon votre projet) afin de s’y retrouver plus facilement car sur des gros projets, on peut facilement arriver à plusieurs dizaines de queries. Pour ça nous allons nous baser sur les Query Options et (Infinite Query Options)

import { queryOptions } from '@tanstack/react-query'
import ApiEvents from '@/src/api/Events.api'
import { toMilliseconds } from '@/src/utils' 

export const eventQueries = {
    getEventList: () =>
        queryOptions({
            queryKey: ['event', 'current'],
            queryFn: () => {
                return ApiEvents.getEventList()
            },
        }),
    getEventDetails: (eventId: string) =>
        queryOptions({
            queryKey: ['event', 'details', eventId],
            queryFn: () => {
                return ApiEvents.getEventDetails(eventId)
            },
            staleTime: toMilliseconds({ hours: 1})
        })
}

Dans cet exemple j’ai créé une “Query Factory” pour gérer mes appels API consacrés à l’affichage des événements de mon application. Et voici comment l’utiliser dans mes composants.

import React from 'react'
import { useQuery } from '@tanstack/react-query'
import { eventQueries } from '@/src/lib/queries'

type Props = {
  eventId: string
}

export default function EventDetailsPage(props: Props) {
  const { data, isLoading, /* etc ...*/ } = useQuery(
    eventQueries.getEventDetails(props.eventId)
  )

  return (
    <div>
      {/* ... */}
    </div>
  )
}

Plutôt simple non ? Et ce système de “Query Factory” permet également de personnaliser plus facilement les queries de ma factory. Admettons que je veuille changer le temps du cache dans un composant en particulier, je peux très bien personnaliser ma query de telle manière.

const { data, isLoading, /* etc ...*/ } = useQuery({
    ...eventQueries.getEventDetails(props.eventId),
    gcTime: toMilliseconds({ minutes: 20 })
})

En plus d’avoir pu créer des queries structurées et personnalisables à l’usage, il est possible de récupérer facilement la clé de la query si l’on souhaite faire des invalidation de caches ou déclencher de nouveaux appels directement depuis useQueryClient.

import { useQueryClient } from '@tanstack/react-query'
import { eventQueries } from '@/src/lib/queries'

const queryClient = useQueryClient()

queryClient.queryClient.invalidateQueries({
  queryKey: eventQueries.getEventList().queryKey,
  exact: true,
})

En bref, l’écriture de la factory n’est pas compliquée et le code pour l’utiliser non plus. Cependant je garde quand même un maximum de souplesse et un typage correct.

Architecture du code

Au niveau architecture du code c’est plutôt simple, nous avons un fichier de queries par “module” de l’application

src/lib/queries
L index.ts             /* contient l'export de toutes fichiers *.queries.ts */
L event.queries.ts     /* contient les queries du module "event" */
L account.queries.ts   /* contient les queries du module "account" */
L ...

Ainsi qu’un fichier index.ts qui s’occupe d’exposer les différents fichiers de queries globalement.

import { eventQueries } from './event.queries.ts'
import { accountQueries } from './account.queries.ts'

export default {
  event: eventQueries,
  account: accountQueries,
}

De cette manière, tout le code du dossier src/lib/queries pourrait être exporté dans un package (ou workspace) séparé pour être utilisé dans plusieurs application par exemple.

Résumé des avantages

  • Un code clair et simple à mettre en place, peu importe le framework JS
  • Ne dépend d’aucune autre lib que “tanstack-query”
  • Code maintenable et permet un clean du code facilement
  • Query factory simple à exporter dans un package séparé

Et vous, comment gérez vous vos queries dans votre application ?

auteur

Écrit par

Alexandre BOREL
Lead Frontend & Mobile Developer