Skip to main content
Notre architecture repose sur un backend client type-safe avec DTOs partagés, combiné à une gestion d’état hybride claire entre données serveur et état client.

Vue d’ensemble

La gestion d’état suit une architecture en trois couches complémentaires :
Principe fondamental : Toutes les données proviennent du backend client. Aucun appel direct à l’API.

Backend Client

Configuration et utilisation

Le ChataigneClient est importé depuis le package @chataigne/client et instancié dans chaque service avec l’authentification :
import { ChataigneClient } from "@chataigne/client";
import { authClient } from "@/lib/auth-client";

// Instance locale dans chaque service
const client = new ChataigneClient(authClient.$fetch);

Utilisation dans les services

Les services orchestrent les appels au backend client et convertissent les données si nécessaire.
import { ChataigneClient, CreateCatalogDTO } from "@chataigne/client";
import { authClient } from "@/lib/auth-client";

const client = new ChataigneClient(authClient.$fetch);

export const createCatalog = async (data: CreateCatalogDTO) => {
  return client.location.catalog.create(data);
};

export const getLocationCatalogs = async (locationId: string) => {
  return client.location.getLocationCatalogs(locationId);
};

Avantages clés

Les DTOs sont partagés entre frontend et backend. Toute modification du contrat API provoque une erreur de compilation.
// ✅ TypeScript vérifie automatiquement les types
const catalog = await client.location.catalog.create({
  name: "Menu Été",
  locationId: "loc-123",
});

// ✅ Autocomplétion et validation des propriétés
const catalogs = await client.location.getLocationCatalogs("loc-123");
Structure uniforme pour tous les appels :
// Pattern cohérent pour toutes les ressources
client.location.catalog.create(data);
client.location.catalog.update(catalogId, name);
client.location.catalog.delete(catalogId);
client.location.getLocationCatalogs(locationId);
Toutes les erreurs passent par le même mécanisme, facilitant le debugging et la gestion globale :
// Gestion des erreurs automatique et cohérente
const catalogs = await client.location.getLocationCatalogs(locationId);
return catalogs;

TanStack Query

TanStack Query résout les défis du cache et de la synchronisation des données serveur. Il évite les appels API redondants, garde nos données fraîches automatiquement, et gère les états de chargement/erreur pour nous.
TanStack Query gère le cache et la synchronisation de toutes les données provenant du backend client.

Configuration

1

Configuration du QueryClient

Configuration simple pour démarrer :
// config/query.config.ts
import { QueryClient } from "@tanstack/react-query";

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      retry: 3,
    },
  },
});
2

Provider dans l'application

Intégration dans l’application :
// app/providers.tsx
export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

Patterns de hooks

Les custom hooks encapsulent la logique TanStack Query et fournissent une interface simple pour nos composants. Ils transforment nos services en hooks réactifs avec cache automatique.Pattern pour récupérer des données via le backend client :
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),
});
}

Utilisation dans les composants

Nos composants consomment les hooks de données sans connaître les détails de TanStack Query. Ils reçoivent des données typées, des états de chargement et bénéficient automatiquement du cache. Exemple d’intégration simple :
export function CatalogList({ locationId }: { locationId: string }) {
  const { data: catalogs, isLoading } = useCatalogs(locationId);

  if (isLoading) return <CatalogListSkeleton />;

  return (
    <div className="space-y-4">
      {catalogs?.map((catalog) => (
        <CatalogCard key={catalog.id} catalog={catalog} />
      ))}
    </div>
  );
}

Zustand

Zustand gère l’état client qui ne vient pas du serveur : préférences utilisateur, état des interfaces (sidebar ouverte/fermée), filtres temporaires et données de formulaires en cours de saisie.
Zustand ne doit jamais dupliquer des données serveur. Son rôle est complémentaire à TanStack Query.

Store par feature

On crée un store Zustand par feature pour isoler son état UI. Cela évite les conflits entre features et facilite la maintenance. Pattern pour l’état UI d’une feature :
import { create } from "zustand";
import { CatalogDTO, ProductDTO } from "@chataigne/client";

interface CatalogState {
  catalog: CatalogDTO | null;
  selectedCatalogId: string | null;

  // Actions
  setCatalog: (catalog: CatalogDTO) => void;
  setSelectedCatalogId: (id: string | null) => void;
  updateProduct: (updatedProduct: ProductDTO) => void;
  addProduct: (newProduct: ProductDTO) => void;
}

export const useCatalogStore = create<CatalogState>()((set, get) => ({
  catalog: null,
  selectedCatalogId: null,

  setCatalog: (catalog) => set({ catalog }),
  setSelectedCatalogId: (id) => set({ selectedCatalogId: id }),

  updateProduct: (updatedProduct) =>
    set((state) => ({
      catalog: state.catalog
        ? {
            ...state.catalog,
            products: [
              ...state.catalog.products.filter(
                (p) => p.id !== updatedProduct.id
              ),
              updatedProduct,
            ],
          }
        : null,
    })),

  addProduct: (newProduct) =>
    set((state) => ({
      catalog: state.catalog
        ? {
            ...state.catalog,
            products: [...state.catalog.products, newProduct],
          }
        : null,
    })),
}));

Store global

Le store global contient les préférences qui affectent toute l’application : thème, langue, paramètres utilisateur. On utilise la persistance pour conserver ces données entre les sessions. Pour l’état vraiment global de l’application :
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";

interface AppState {
  theme: "light" | "dark";

  // Actions
  toggleTheme: () => void;
}

export const useAppStore = create<AppState>()(
  persist(
    (set) => ({
      theme: "light",

      toggleTheme: () =>
        set((state) => ({
          theme: state.theme === "light" ? "dark" : "light",
        })),
    }),
    {
      name: "app-storage",
      storage: createJSONStorage(() => localStorage),
    }
  )
);

Intégration complète

Voici comment on fait collaborer les trois couches dans un composant réel. Les données serveur viennent de TanStack Query, l’état UI de Zustand, et la logique métier combine les deux. Exemple d’un composant utilisant les trois couches de l’architecture :
export function CatalogDashboard({ locationId }: { locationId: string }) {
  // 📊 Données serveur via TanStack Query + Backend Client
  const { data: catalogs } = useCatalogs(locationId);

  // 🎨 État client via Zustand
  const { selectedCatalogId, setSelectedCatalogId } = useCatalogStore();

  // 🔄 Logique dérivée
  const selectedCatalog = catalogs?.find(
    (catalog) => catalog.id === selectedCatalogId
  );

  return (
    <div className="space-y-6">
      <select onChange={(e) => setSelectedCatalogId(e.target.value)}>
        {catalogs?.map((catalog) => (
          <option key={catalog.id} value={catalog.id}>
            {catalog.name}
          </option>
        ))}
      </select>

      {selectedCatalog && <CatalogDetails catalog={selectedCatalog} />}
    </div>
  );
}

Bonnes pratiques

1

Backend Client

• Toujours passer par le backend client pour les appels API• Ne jamais faire d’appels directs aux endpoints• Utiliser les types partagés pour la type safety• Centraliser la gestion des erreurs
2

TanStack Query

• Utiliser exclusivement pour les données serveur• Structurer les query keys de manière cohérente• Invalider le cache après les mutations• Gérer les états de loading et d’erreur
3

Zustand

• Réserver à l’état UI local uniquement• Ne jamais dupliquer les données serveur• Créer un store par feature pour l’isolation• Utiliser la persistance pour les préférences globales
4

Query Keys

• Structure hiérarchique : ["resource", id, "subresource"]• Cohérence dans toute l’application• Éviter les clés trop génériques• Faciliter l’invalidation ciblée