Leçon 4 / 20

Séparer les accès aux données de l’interface

12 min React — architecture, performance et accessibilité Gratuit

Objectif : Isoler le chargement et l’enregistrement des dépenses derrière une interface de service typée.

Quand une interface lit directement le stockage du navigateur, elle devient dépendante de son format et de ses erreurs : une donnée mal formée peut interrompre l’affichage, et remplacer le stockage devient difficile. Vous allez placer ces opérations derrière un service typé. Les composants pourront ensuite demander des dépenses sans connaître les détails de leur persistance.

Définir la responsabilité du service

Vous avez déjà décidé que les dépenses et les catégories auront une source commune, tandis que les totaux seront calculés à partir des données disponibles. Le service ne devient pas cette source d’état : il fournit une frontière entre le code de l’application et le mécanisme qui conserve les données. Dans cette leçon, vous implémentez cette frontière pour les dépenses, sans encore modifier les composants ni ajouter de contexte.

Le contrat de service décrit deux opérations asynchrones : charger les dépenses et enregistrer une nouvelle dépense. Il utilise les types métier déjà définis dans src/types/depense.ts. L’interface du service ne reçoit aucun élément HTML et ne prend aucune décision de présentation.

src/services/depensesService.tsTypeScript
import type {
  Depense,
  NouvelleDepense,
} from "../types/depense";

const CLE_STOCKAGE = "atelier-depenses";

export interface DepensesService {
  charger(): Promise<Depense[]>;
  enregistrer(nouvelleDepense: NouvelleDepense): Promise<Depense>;
}

function estDepense(valeur: unknown): valeur is Depense {
  if (typeof valeur !== "object" || valeur === null) {
    return false;
  }

  const depense = valeur as Record<string, unknown>;

  return (
    typeof depense.id === "string" &&
    typeof depense.description === "string" &&
    typeof depense.montant === "number" &&
    Number.isFinite(depense.montant) &&
    typeof depense.categorie === "string" &&
    typeof depense.date === "string"
  );
}

function lireStockage(): Depense[] {
  const contenu = window.localStorage.getItem(CLE_STOCKAGE);

  if (contenu === null) {
    return [];
  }

  try {
    const donnees: unknown = JSON.parse(contenu);

    if (!Array.isArray(donnees) || !donnees.every(estDepense)) {
      return [];
    }

    return donnees;
  } catch {
    return [];
  }
}

function creerIdentifiant(): Depense["id"] {
  return crypto.randomUUID();
}

export const depensesService: DepensesService = {
  async charger() {
    return lireStockage();
  },

  async enregistrer(nouvelleDepense) {
    const depense: Depense = {
      ...nouvelleDepense,
      id: creerIdentifiant(),
    };
    const depenses = lireStockage();

    window.localStorage.setItem(
      CLE_STOCKAGE,
      JSON.stringify([...depenses, depense]),
    );

    return depense;
  },
};

L’interface DepensesService fixe les signatures attendues ; depensesService en fournit une implémentation qui utilise localStorage. Ce fichier joue le rôle d’adaptateur de persistance : si vous remplacez plus tard le stockage du navigateur par une API, les appels de l’application pourront conserver le même contrat. Les méthodes sont asynchrones, ce qui permet au contrat de rester adapté à un stockage réseau éventuel.

Ne pas confondre type et données valides

JSON.parse peut produire n’importe quelle valeur JavaScript, même si le programme attend ensuite un tableau de dépenses. Le service la reçoit donc d’abord comme unknown, puis vérifie la structure de chaque entrée avec estDepense. Si le contenu est invalide ou illisible, le chargement renvoie une liste vide plutôt que de faire circuler des données non vérifiées.

L’identifiant est attribué lors de l’enregistrement, et non par le formulaire. La méthode retourne la dépense créée avec cet identifiant : un futur appelant pourra l’utiliser pour mettre à jour la source de données détenue par l’application. L’enregistrement conserve les dépenses déjà valides et ajoute la nouvelle entrée sans modifier le tableau lu.

Vérifier le contrat sans le lier à l’interface

Le fichier dépend uniquement des types du domaine et des fonctions natives du navigateur. Il est donc possible d’en vérifier le contrat indépendamment du rendu React. À ce stade, aucun appel au service n’est ajouté à App.tsx : le chargement et la coordination avec les dépenses et catégories partagées seront introduits avec les hooks et le contexte des prochaines leçons.

À retenir

  • Placez les opérations de persistance dans le service, pas dans les composants d’interface.
  • Exposez un contrat typé qui décrit les opérations offertes par le service.
  • Traitez les données lues comme inconnues et vérifiez-les avant de les retourner comme dépenses.
  • Gardez le stockage remplaçable derrière l’adaptateur, sans introduire ici l’état partagé de l’application.

Envie d'aller plus loin ?

Les cours Enovatic sont gratuits et le resteront. Si vous cherchez un accompagnement encadré, nos formations professionnelles sont animées par les mêmes formateurs.