Skip to main content
Les hooks personnalisés sont le cœur de notre architecture. Ils encapsulent toute la logique métier, orchestrent TanStack Query et React Hook Form, et fournissent une interface simple aux composants.

Principe fondamental

Les hooks custom séparent complètement la logique métier de l’affichage. Nos composants deviennent de simples orchestrateurs visuels qui consomment des hooks intelligents.
Règle d’or : Aucune logique métier dans les composants. Tout passe par les hooks.

Hooks de données

Hooks de lecture (Queries)

Les hooks de lecture encapsulent TanStack Query et fournissent des données typées avec gestion automatique du cache.
Pattern de base pour récupérer des données :
use-catalogs.hook.ts
import { useQuery } from "@tanstack/react-query";
import { getLocationCatalogs } from "../services/catalog.service";

export function useCatalogs(locationId: string) {
  return useQuery({
    queryKey: ["catalogs", locationId],
    queryFn: () => getLocationCatalogs(locationId),
    enabled: !!locationId,
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}

Hooks de mutation

Les mutations gèrent les opérations de création, modification et suppression avec invalidation automatique du cache.
Pattern de base pour modifier des données :
use-create-catalog.hook.ts
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { createCatalog } from "../services/catalog.service";
import type { CreateCatalogDTO } from "@chataigne/client";

export function useCreateCatalog() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: CreateCatalogDTO) => createCatalog(data),
    onSuccess: (newCatalog) => {
      // Invalidation ciblée
      queryClient.invalidateQueries({
        queryKey: ["catalogs", newCatalog.locationId],
      });

      // Ajout direct au cache
      queryClient.setQueryData(["catalogs", newCatalog.id], newCatalog);
    },
    onError: (error) => {
      console.error("Erreur lors de la création:", error);
    },
  });
}

Patterns avancés

1

Prefetching

Anticiper les besoins de données :
export function usePrefetchCatalog() {
  const queryClient = useQueryClient();

  return (catalogId: string) => {
    queryClient.prefetchQuery({
      queryKey: ["catalogs", catalogId],
      queryFn: () => getCatalog(catalogId),
      staleTime: 10 * 1000,
    });
  };
}
2

Infinite queries

Pagination infinie :
export function useInfiniteProducts(catalogId: string) {
  return useInfiniteQuery({
    queryKey: ["products", catalogId, "infinite"],
    queryFn: ({ pageParam = 0 }) =>
      getProductsPaginated(catalogId, pageParam),
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    enabled: !!catalogId,
  });
}
3

Dependent queries

Requêtes en cascade :
export function useCatalogWithProducts(catalogId: string) {
  const { data: catalog } = useCatalog(catalogId);

  const { data: products } = useQuery({
    queryKey: ["products", catalogId],
    queryFn: () => getProducts(catalogId),
    enabled: !!catalog, // Attend que le catalogue soit chargé
  });

  return { catalog, products };
}

Hooks de formulaires

Hook de formulaire de base

Intégration React Hook Form + Zod pour la validation :
use-catalog-form.hook.ts
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
  catalogSchema,
  type CatalogSchema,
} from "../validations/catalog.schema";
import {
  useCreateCatalog,
  useUpdateCatalog,
} from "./use-catalog-mutations.hook";

interface UseCatalogFormProps {
  catalogId?: string;
  initialData?: Partial<CatalogSchema>;
  onSuccess?: () => void;
}

export function useCatalogForm({
  catalogId,
  initialData,
  onSuccess,
}: UseCatalogFormProps = {}) {
  const createCatalog = useCreateCatalog();
  const updateCatalog = useUpdateCatalog();

  const form = useForm<CatalogSchema>({
    resolver: zodResolver(catalogSchema),
    defaultValues: {
      name: "",
      description: "",
      ...initialData,
    },
  });

  const isEditing = !!catalogId;
  const mutation = isEditing ? updateCatalog : createCatalog;

  const onSubmit = async (data: CatalogSchema) => {
    try {
      if (isEditing) {
        await updateCatalog.mutateAsync({ id: catalogId, data });
      } else {
        await createCatalog.mutateAsync(data);
      }

      onSuccess?.();
      form.reset();
    } catch (error) {
      form.setError("root", {
        message: "Une erreur est survenue lors de la sauvegarde",
      });
    }
  };

  return {
    ...form,
    onSubmit: form.handleSubmit(onSubmit),
    isSubmitting: mutation.isPending,
    isEditing,
  };
}

Hook de formulaire avec validation dynamique

Validation qui dépend du contexte :
use-product-form.hook.ts
import { useForm, useWatch } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { productSchema } from "../validations/product.schema";
import { useCatalogCategories } from "./use-catalog-categories.hook";

export function useProductForm(catalogId: string) {
  const { data: categories } = useCatalogCategories(catalogId);

  const form = useForm({
    resolver: zodResolver(productSchema),
    defaultValues: {
      name: "",
      price: 0,
      categoryId: "",
    },
  });

  // Surveillance des changements pour validation dynamique
  const selectedCategoryId = useWatch({
    control: form.control,
    name: "categoryId",
  });

  // Validation conditionnelle basée sur la catégorie
  const selectedCategory = categories?.find((c) => c.id === selectedCategoryId);

  // Mise à jour des règles de validation selon la catégorie
  useEffect(() => {
    if (selectedCategory?.requiresDescription) {
      form.setValue("description", form.getValues("description") || "");
    }
  }, [selectedCategory, form]);

  return {
    ...form,
    categories,
    selectedCategory,
  };
}

Hooks métier

Hook d’authentification

Orchestration complète de l’authentification :
use-auth.hook.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
import { authService } from "../services/auth.service";
import { useAuthStore } from "../store/auth.store";
import type { LoginSchema } from "../validations/auth.schema";

export function useAuth() {
  const { token, setToken, clearToken } = useAuthStore();
  const queryClient = useQueryClient();
  const router = useRouter();

  // Query pour l'utilisateur courant
  const { data: user, isLoading } = useQuery({
    queryKey: ["auth", "current-user"],
    queryFn: authService.getCurrentUser,
    enabled: !!token,
    staleTime: 5 * 60 * 1000,
    retry: false,
  });

  // Mutation de connexion
  const loginMutation = useMutation({
    mutationFn: (credentials: LoginSchema) => authService.login(credentials),
    onSuccess: (response) => {
      setToken(response.token);
      queryClient.setQueryData(["auth", "current-user"], response.user);
      queryClient.invalidateQueries({ queryKey: ["user"] });
      router.push("/dashboard");
    },
    onError: (error) => {
      console.error("Échec de la connexion:", error);
    },
  });

  // Mutation de déconnexion
  const logoutMutation = useMutation({
    mutationFn: authService.logout,
    onSuccess: () => {
      clearToken();
      queryClient.clear();
      router.push("/login");
    },
  });

  return {
    user,
    token,
    login: loginMutation.mutate,
    logout: logoutMutation.mutate,
    isLoading,
    isLoggingIn: loginMutation.isPending,
    isAuthenticated: !!user && !!token,
  };
}

Hook de gestion complexe

Orchestration de plusieurs sources de données :
use-catalog-management.hook.ts
import { useCatalogs } from "./use-catalogs.hook";
import { useProducts } from "./use-products.hook";
import { useCatalogStore } from "../store/catalog.store";
import {
  useCreateProduct,
  useUpdateProduct,
} from "./use-product-mutations.hook";

export function useCatalogManagement(locationId: string) {
  const { selectedCatalogId, setSelectedCatalogId } = useCatalogStore();

  // Données
  const { data: catalogs, isLoading: catalogsLoading } =
    useCatalogs(locationId);
  const { data: products, isLoading: productsLoading } = useProducts(
    selectedCatalogId || ""
  );

  // Mutations
  const createProduct = useCreateProduct();
  const updateProduct = useUpdateProduct();

  // Logique dérivée
  const selectedCatalog = catalogs?.find((c) => c.id === selectedCatalogId);
  const isLoading = catalogsLoading || productsLoading;

  // Actions complexes
  const selectCatalog = (catalogId: string) => {
    setSelectedCatalogId(catalogId);
  };

  const addProductToCatalog = async (productData: CreateProductDTO) => {
    if (!selectedCatalogId) return;

    await createProduct.mutateAsync({
      ...productData,
      catalogId: selectedCatalogId,
    });
  };

  // Auto-sélection du premier catalogue
  useEffect(() => {
    if (catalogs?.length && !selectedCatalogId) {
      setSelectedCatalogId(catalogs[0].id);
    }
  }, [catalogs, selectedCatalogId, setSelectedCatalogId]);

  return {
    // État
    catalogs,
    products,
    selectedCatalog,
    selectedCatalogId,
    isLoading,

    // Actions
    selectCatalog,
    addProductToCatalog,

    // Mutations
    createProduct: createProduct.mutate,
    updateProduct: updateProduct.mutate,

    // États des mutations
    isCreatingProduct: createProduct.isPending,
    isUpdatingProduct: updateProduct.isPending,
  };
}

Bonnes pratiques

1

Naming et organisation

• Préfixe use- pour tous les hooks• Suffixe .hook.ts pour les fichiers• Un hook par fichier avec export nommé• Grouper par feature dans /hooks
2

Query keys cohérentes

• Structure hiérarchique : ["resource", id, "params"]• Utiliser des constantes pour éviter les erreurs• Faciliter l’invalidation ciblée• Documenter la structure des clés
3

Gestion des erreurs

• Toujours gérer les cas d’erreur• Messages d’erreur utilisateur-friendly• Logging approprié pour le debugging• Fallbacks et états de récupération
4

Performance

• Utiliser enabled pour les requêtes conditionnelles• Optimiser staleTime selon le contexte• Prefetching pour les données prévisibles• Optimistic updates pour l’UX
Les hooks personnalisés sont la clé d’une architecture maintenable. Ils encapsulent la complexité et offrent une interface simple aux composants.