Aller au contenu

Create API Hook

Crée un hook React Query pour récupérer des données depuis l'API.

Arguments

  • endpoint (requis) : L'endpoint à utiliser (depuis @bricks-common/api-communication)
  • queryKey (requis) : La clé de query (depuis allStaticQueryKeys)

Instructions

  1. Demander à l'utilisateur :
  2. Le queryKey est-il statique ou dynamique (avec paramètres) ?
  3. Est-ce une query simple ou paginée (infinite query) ?
  4. Si le validator n'est pas évident par le nom du hook : quel validator utiliser ?

    • Un endpoint existant (get{{EndpointName}}Endpoint.response)
    • Un validator custom à créer
  5. Déterminer le chemin :

  6. src/api/{feature}/use{HookName}.ts

  7. Créer le hook selon le pattern approprié.

Architecture

Le data fetching a deux étapes de validation (gérées automatiquement par useValidatedQuery / useInfiniteValidatedQuery) :

  1. processHttpResult (dans queryFn) - Valide la réponse API, stocke les données brutes en cache
  2. select (interne) - Re-valide les données du cache, reconstruit les types riches (Cents, NanoCents, Date)

Templates

Hook Simple (QueryKey Statique)

import { get{{EndpointName}}Endpoint } from "@bricks-common/api-communication";

import { allStaticQueryKeys } from "@providers/ReactQueryProvider";
import { useValidatedQuery } from "@utils/api/useValidatedQuery";
import { customAxios } from "@utils/axios/axios";

export const use{{HookName}} = () =>
  useValidatedQuery({
    queryKey: allStaticQueryKeys.{{queryKeyName}},
    validator: get{{EndpointName}}Endpoint.response,
    queryFn: () => customAxios.get(get{{EndpointName}}Endpoint.request.path),
  });

Hook Simple (QueryKey Dynamique)

import { get{{EndpointName}}Endpoint, type UUID } from "@bricks-common/api-communication";

import { allStaticQueryKeys } from "@providers/ReactQueryProvider";
import { useValidatedQuery } from "@utils/api/useValidatedQuery";
import { customAxios } from "@utils/axios/axios";

export const use{{HookName}} = (id: UUID) =>
  useValidatedQuery({
    queryKey: allStaticQueryKeys.{{queryKeyName}}(id),
    validator: get{{EndpointName}}Endpoint.response,
    queryFn: () => customAxios.get(get{{EndpointName}}Endpoint.request.path(id)),
  });

Hook Simple avec Options

import { get{{EndpointName}}Endpoint } from "@bricks-common/api-communication";

import { allStaticQueryKeys } from "@providers/ReactQueryProvider";
import { useValidatedQuery } from "@utils/api/useValidatedQuery";
import { customAxios } from "@utils/axios/axios";

export const use{{HookName}} = (params?: { enabled?: boolean }) =>
  useValidatedQuery({
    queryKey: allStaticQueryKeys.{{queryKeyName}},
    validator: get{{EndpointName}}Endpoint.response,
    queryFn: () => customAxios.get(get{{EndpointName}}Endpoint.request.path),
    showErrorNotification: false,
    enabled: params?.enabled,
    retry: 0,
  });

Hook Infinite Query (Pagination)

import { get{{EndpointName}}Endpoint, type {{ParamsType}}, type {{ResponseType}} } from "@bricks-common/api-communication";

import { allStaticQueryKeys } from "@providers/ReactQueryProvider";
import { useInfiniteValidatedQuery } from "@utils/api/useInfiniteValidatedQuery";
import { customAxios } from "@utils/axios/axios";

const PAGE_SIZE = 10;

const getNextPageParam = (lastPage: {{ResponseType}}, allPages: {{ResponseType}}[]) => {
  if (!Array.isArray(lastPage.data) || lastPage.data.length < PAGE_SIZE) {
    return undefined;
  }
  return allPages.length * PAGE_SIZE;
};

export const use{{HookName}} = (filters: {{ParamsType}}) =>
  useInfiniteValidatedQuery({
    queryKey: allStaticQueryKeys.{{queryKeyName}}(filters),
    validator: get{{EndpointName}}Endpoint.response,
    queryFn: ({ pageParam }) =>
      customAxios.get(get{{EndpointName}}Endpoint.request.path, {
        params: { ...filters, cursor: pageParam, take: PAGE_SIZE },
      }),
    initialPageParam: 0,
    getNextPageParam,
  });

Hook Infinite Query avec Select Custom

import { get{{EndpointName}}Endpoint, type {{ParamsType}}, type {{ResponseType}} } from "@bricks-common/api-communication";

import { allStaticQueryKeys } from "@providers/ReactQueryProvider";
import { useInfiniteValidatedQuery } from "@utils/api/useInfiniteValidatedQuery";
import { customAxios } from "@utils/axios/axios";

const PAGE_SIZE = 10;

const getNextPageParam = (lastPage: {{ResponseType}}, allPages: {{ResponseType}}[]) => {
  if (!Array.isArray(lastPage.data) || lastPage.data.length < PAGE_SIZE) {
    return undefined;
  }
  return allPages.length * PAGE_SIZE;
};

export const use{{HookName}} = (filters: {{ParamsType}}) =>
  useInfiniteValidatedQuery({
    queryKey: allStaticQueryKeys.{{queryKeyName}}(filters),
    validator: get{{EndpointName}}Endpoint.response,
    queryFn: ({ pageParam }) =>
      customAxios.get(get{{EndpointName}}Endpoint.request.path, {
        params: { ...filters, cursor: pageParam, take: PAGE_SIZE },
      }),
    initialPageParam: 0,
    getNextPageParam,
    // select reçoit les pages déjà validées (TPage[])
    select: (validatedPages) => ({
      pages: validatedPages,
      flatData: validatedPages.flatMap((page) => page.data),
    }),
  });

Options Disponibles

useValidatedQuery

Option Type Description
queryKey QueryKey Clé de cache React Query
validator Validator<T> Validator idonttrustlikethat
queryFn () => Promise<AxiosResponse> Fonction qui retourne la réponse Axios
showErrorNotification boolean Afficher un toast en cas d'erreur (défaut: true)
enabled, retry, staleTime, etc. - Toutes les options standard de useQuery

useInfiniteValidatedQuery

Option Type Description
queryKey QueryKey Clé de cache React Query
validator Validator<TPage> Validator pour chaque page
queryFn ({ pageParam }) => Promise<AxiosResponse> Fonction qui retourne la réponse Axios
getNextPageParam (lastPage, allPages) => number \| undefined Calcul du prochain curseur
initialPageParam number Curseur initial (généralement 0)
showErrorNotification boolean Afficher un toast en cas d'erreur (défaut: true)
select (validatedPages: TPage[]) => TSelect Transformation des pages validées

Cas Spéciaux

Hook avec Logique Conditionnelle (useQuery direct)

Pour les hooks qui nécessitent une logique conditionnelle complexe (ex: retourner null sans appel API), utiliser useQuery directement avec createSelectValidator :

import { userValidator } from "@bricks-common/api-communication";
import { useQuery } from "@tanstack/react-query";

import { allStaticQueryKeys } from "@providers/ReactQueryProvider";
import { processHttpResult } from "@utils/api/processHttpResult";
import { createSelectValidator } from "@utils/api/selectValidator";
import { customAxios } from "@utils/axios/axios";

const selectUser = createSelectValidator(userValidator, allStaticQueryKeys.getMe);

export const useMe = () =>
  useQuery({
    queryKey: allStaticQueryKeys.getMe,
    queryFn: () =>
      processHttpResult({
        responsePromise: customAxios.get("/customers/me"),
        validator: userValidator,
      }),
    select: selectUser,
    enabled: !!token, // Conditionnel
  });

Usage dans les Composants

const { data, isPending, isError } = useMyData();

// Quand isPending || isError → afficher skeleton/erreur
// Quand les deux sont false → data est garanti défini (TypeScript le sait)
{isPending || isError ? (
  <Skeleton />
) : (
  <Content data={data} /> // data est T, pas T | undefined
)}

Checklist finale

  • [ ] Utiliser useValidatedQuery ou useInfiniteValidatedQuery (sauf cas spécial)
  • [ ] queryFn retourne AxiosResponse (pas le résultat de processHttpResult)
  • [ ] QueryKey ajouté dans allStaticQueryKeys si nouveau
  • [ ] showErrorNotification: false si l'erreur est gérée autrement

Règles appliquées

  • front-api-data-fetching.mdc
  • front-state-management.mdc