Architecture

Cette page décrit l'organisation des fichiers d'une application SmartMaker. Contrairement à un projet React ordinaire, l'emplacement des fichiers y a des conséquences techniques : il conditionne la capacité à extraire une fonctionnalité vers une autre application, et un mauvais découpage de la couche base de données provoque une erreur de chargement difficile à diagnostiquer.

Vue d'ensemble

mobile/
├── public/
│   ├── images/              # icones PWA et images statiques
│   └── locales/
│       └── <lang>/
│           └── <feature>.json   # un fichier par langue ET par feature
├── src/
│   ├── api/
│   │   ├── index.js
│   │   └── mapping/
│   │       └── <feature>.js     # mapping backend <-> front, un par feature
│   ├── db/
│   │   ├── index.js             # instanciation de Db
│   │   └── stores/
│   │       └── <feature>/
│   │           ├── indexes.js   # schema Dexie, AUCUN import
│   │           ├── useDb<Feature>.jsx   # hook CRUD metier
│   │           └── index.js     # barrel
│   ├── components/
│   │   ├── app/                 # Router, Provider, Head, Toaster
│   │   ├── layouts/             # mises en page partagees
│   │   ├── global/              # composants transverses de l'application
│   │   └── pages/
│   │       ├── public/<Page>/   # pages non authentifiees
│   │       ├── private/<Page>/  # pages authentifiees
│   │       └── errors/<Page>/
│   ├── global-state/
│   │   └── slices/              # etat UI uniquement
│   ├── hooks/                   # hooks transverses de l'application
│   ├── i18n/
│   │   └── index.js
│   ├── utils/
│   │   ├── constants/
│   │   ├── functions/
│   │   └── maps/
│   │       ├── form.jsx         # type de champ -> composant de saisie
│   │       └── list.jsx         # type de colonne -> composant de cellule
│   ├── assets/                  # fichiers pris en charge par la compilation
│   ├── appConfig.js
│   ├── main.jsx                 # point d'entree
│   └── sw.js                    # Service Worker (mode injectManifest)
├── .env                         # variables d'environnement, non versionne
├── .env.example
├── index.html
├── eslint.config.js
├── package.json
└── vite.config.js

Une application générée par SmartBoot arrive avec cette arborescence déjà en place, avec un exemple par emplacement : un store users complet (indexes.js et useDbUsers), un api/mapping/ commenté, les maps form.jsx et list.jsx, et cinq namespaces de traduction. Vous la remplissez, vous n'avez pas à la créer.

Le principe directeur : une fonctionnalité, un dossier

Une fonctionnalité métier (interventions, tâches, devis, inventaire) doit pouvoir être copiée telle quelle vers une autre application SmartMaker. Concrètement, elle se répartit sur trois emplacements et trois seulement :

Emplacement Contenu
db/stores/<feature>/ schéma Dexie et hook CRUD
api/mapping/<feature>.js conversion entre le format backend et le format front
locales/<lang>/<feature>.json traductions du namespace de la fonctionnalité

Le test est simple : copier ces trois éléments vers un autre projet, ajouter la route, et cela doit fonctionner sans autre modification. Si un import "fuit" vers un fichier propre à l'application d'origine, c'est un défaut à corriger.

La couche données

Tout le CRUD dans un hook

Chaque fonctionnalité expose un hook useDb<Feature> qui porte l'intégralité des accès à la base locale.

// src/db/stores/interventions/useDbInterventions.jsx
import { db } from "src/db";

export const useDbInterventions = () => {
  const list = async (filters) => { /* ... */ };
  const get = async (id) => { /* ... */ };
  const create = async (payload) => { /* ... */ };
  const update = async (id, payload) => { /* ... */ };
  const remove = async (id) => { /* ... */ };

  return { list, get, create, update, remove };
};

Le hook ne connaît que la classe Db, useGlobalStates et useApi. Rien d'autre du projet hôte.

Attention

Ne mettez jamais d'appel Dexie directement dans un composant de page. C'est l'anti-pattern le plus fréquent, et c'est celui qui rend une fonctionnalité impossible à extraire ensuite.

Le nom du hook suit la forme useDb<Feature> : useDbInterventions, useDbTasks, useDbProducts. Les formes use<Feature>Services que l'on trouve dans d'anciens projets sont du legacy à renommer.

Le fichier indexes.js, et pourquoi il est obligatoire

Important

Le schéma Dexie de chaque fonctionnalité doit vivre dans un fichier indexes.js sans aucun import, et db/index.js doit l'importer directement, sans passer par le barrel.

Sans cette séparation, le graphe d'imports forme un cycle :

db/index.js
  -> import { tasksIndexes } from "./stores"        (barrel)
  -> stores/tasks/index.js
       export const tasksIndexes = "..."
       export * from "./useDbTasks"                 <- tire le hook
  -> useDbTasks.jsx
       import { db } from "src/db"                  <- retour au depart

À la compilation, ce cycle est partiellement remonté par Vite et produit une erreur Cannot access 'tasksIndexes' before initialization au chargement du bundle. Le symptôme est cruel : npm run build réussit, puisqu'il ne charge pas le bundle, et l'application plante au démarrage avec une erreur qui semble venir d'ailleurs (authentification cassée, boucle de connexion).

La parade, qui rompt le cycle en faisant de indexes.js une feuille du graphe :

// src/db/stores/tasks/indexes.js -- ZERO import
export const tasksIndexes = "++id, ref, status, updatedAt";
// src/db/index.js -- importe les FEUILLES, pas le barrel
import { tasksIndexes } from "./stores/tasks/indexes";
import { projectsIndexes } from "./stores/projects/indexes";

export const db = new Db({
  name: "myapp",
  version: 1,
  stores: { tasks: tasksIndexes, projects: projectsIndexes },
}).db;

export * from "./stores";   // acceptable ici : db est deja construit

Le barrel stores/<feature>/index.js reste utilisable normalement par les pages et les autres hooks. Seul db/index.js doit court-circuiter.

Où va quelle donnée

Nature Emplacement
donnée métier persistée (interventions, devis, photos) Db (Dexie), via useDb<Feature>
état d'interface persistant (thème, langue, filtres, dernière page) useGlobalStates (Redux et redux-persist)
état d'interface éphémère (formulaire en cours, modale ouverte) useState ou useStates local

Attention

Ne placez jamais une liste d'objets métier dans un slice Redux. Elle doit vivre dans Dexie, et les composants la lisent via le hook de la fonctionnalité.

Organiser les composants

C'est la question qui revient le plus souvent. La règle de classement tient en une phrase : on range un composant selon sa portée, pas selon son sujet.

Dossier Ce qu'on y met Test d'appartenance
components/app/ la plomberie de l'application : Router, composition des providers, Head, Toaster il n'y en a qu'un seul exemplaire dans l'application
components/layouts/ mises en page partagées par plusieurs pages il enveloppe des pages, il n'en est pas une
components/global/ composants transverses réutilisés par plusieurs pages il est importé par au moins deux pages
components/pages/<visibilité>/<Page>/ une page, montée sur une route il correspond à une entrée du Router

Les pages se rangent ensuite par visibilité : public/ pour ce qui est accessible sans authentification, private/ pour le reste, errors/ pour les pages d'erreur. Ce découpage n'est pas décoratif : il reflète ce que protège le RouteGuard et rend immédiatement visible qu'une page est exposée.

Un composant, un dossier

Chaque composant a son propre dossier avec un index.jsx, même s'il est seul. Cela permet d'ajouter plus tard, au même endroit, ses styles, ses sous-composants et ses tests, sans rien déplacer ni corriger un seul import.

components/pages/private/InterventionPage/
├── index.jsx          # la page
├── Header/
│   └── index.jsx      # sous-composant propre a cette page
└── LinesTable/
    └── index.jsx

Astuce

Un sous-composant utilisé par une seule page reste dans le dossier de cette page. Il ne monte dans components/global/ que le jour où une deuxième page l'utilise. Remonter par anticipation encombre l'espace commun de composants qui ne sont partagés par personne.

Une page ne doit rien savoir de l'application

Une page métier doit pouvoir être montée dans le routeur d'une autre application sans modification.

À faire :

  • recevoir ses dépendances par les hooks SmartCommon (useNavigation, useApi, useGlobalStates) ou par ses props
  • passer les composants de formulaire par la map utils/maps/form.jsx, pour que l'application hôte puisse les surcharger par type de champ
  • exposer les chemins de navigation dans une fonction, plutôt que d'écrire navigate('/interventions/123') en dur

À ne pas faire :

  • importer appConfig depuis une page métier
  • lire un slice Redux propre à l'application hôte

Traductions : un namespace par fonctionnalité

public/locales/
├── fr/
│   ├── common.json
│   ├── interventions.json
│   └── products.json
└── en/
    ├── common.json
    ├── interventions.json
    └── products.json
const { t } = useTranslation('interventions');

Tous les namespaces doivent être déclarés dans la configuration i18next pour être préchargés : c'est indispensable au fonctionnement hors ligne, le precache du Service Worker les embarquant via son motif **/*.json.

Attention

Sans Suspense, ajoutez react: { useSuspense: false } à la configuration i18next. Sinon une page atteinte par navigation directe avant la fin du chargement de son namespace reste figée sur les clés brutes.

Le keyPrefix ne doit jamais être écrit en dur dans un hook réutilisable : il se reçoit en paramètre, avec une valeur par défaut.

// A eviter
const useIntStatuses = () => useTranslation(undefined, { keyPrefix: 'intStatuses' });

// Preferable
const useIntStatuses = (keyPrefix = 'intStatuses') => useTranslation(undefined, { keyPrefix });

Configuration : aucune valeur en dur

Tout ce qui change d'un projet ou d'un environnement à l'autre passe par une variable d'environnement Vite ou par une prop de provider.

Variable Rôle
VITE_API_URL URL du backend ; forcée en relatif au build de production
VITE_APP_NAME nom de l'application
VITE_APP_VERSION version et numéro de build, injectés par le Makefile
VITE_LOCALES liste des langues, dérivée des sous-dossiers de public/locales/

appConfig.js peut centraliser ces valeurs, mais ne doit être importé que depuis la coquille de l'application, jamais depuis une fonctionnalité métier.

Les fichiers à la racine de mobile/

Fichier Rôle
.env variables d'environnement, ignoré par Git
.env.example modèle du .env, versionné
index.html page dans laquelle React monte l'application ; porte le lien vers le manifest
vite.config.js configuration de Vite, y compris celle de la PWA
eslint.config.js configuration du lint
package.json dépendances et scripts

Checklist de relecture

À passer avant d'ouvrir une demande de fusion, ou en reprenant un projet existant :

  • [ ] tout le CRUD métier est dans useDb<Feature>, aucun appel Dexie dans une page
  • [ ] chaque db/stores/<feature>/ a son indexes.js sans import, et db/index.js importe les feuilles
  • [ ] le mapping backend/front est isolé dans api/mapping/<feature>.js
  • [ ] aucune donnée métier dans un slice Redux
  • [ ] aucune page métier n'importe appConfig ni un slice propre à l'application
  • [ ] un namespace i18n par fonctionnalité, tous déclarés dans la configuration
  • [ ] aucun fetch ni axios : tout passe par useApi
  • [ ] aucune URL ni aucun port en dur
  • [ ] un composant, un dossier ; les sous-composants restent chez leur page tant qu'ils ne servent qu'à elle

Voir aussi