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
appConfigdepuis 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 sonindexes.jssans import, etdb/index.jsimporte 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
appConfigni un slice propre à l'application - [ ] un namespace i18n par fonctionnalité, tous déclarés dans la configuration
- [ ] aucun
fetchniaxios: tout passe paruseApi - [ ] 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
- Composants et pages
- Stockage de données
- Synchronisation offline
- Traductions
- PWA - Service Worker et démarrage hors ligne
- Formation - Bonnes pratiques