Assurez-vous d'utiliser Node.js version 22.
yarn install
yarn run devAccédez ensuite à http://localhost:5173 pour tester.
Nous souhaitons implémenter une application web dont l'interface est entièrement pilotée par le serveur. L'application interroge une API GET /intents/:screenId pour déterminer dynamiquement quels composants afficher et avec quelles données.
Par exemple, pour GET /intents/page-a, l’API renverrait :
{
"address-form": { "default": "16 RUE DE LA VILLE LEVEQUE 75008 PARIS" },
"button": { "label": "Envoyer" }
}Chaque clé représente un composant à afficher, et chaque valeur correspond à ses props.
Dans ce test, l’API n’existe pas encore. Elle est simulée via la fonction fetchIntents disponible dans /src/mock/intents.ts.
| Nom de l’intent | Composant associé |
|---|---|
address-form |
<AddressForm /> |
accept-cgu |
<AcceptCGU /> |
button |
<Button /> |
Les composants sont déjà codés et importables depuis src/components/. Nous prévoyons de supporter à terme entre 80 et 100 intents.
- Appeler
fetchIntents(screenId)pour simuler un appel à l’API. - Pour chaque intent, afficher dynamiquement le composant correspondant avec les bonnes props.
- Respecter l’ordre dans lequel les intents sont retournés.
Vous devez écrire cette logique dans le fichier ScreenRenderer.tsx.
Vous pouvez centraliser toute la logique dans ce fichier. La clarté et la fonctionnalité priment sur la structure.
Certains intents peuvent inclure une condition d'affichage :
{
"address-form": {
"visible-if": { "accept-cgu": true },
"default": "16 RUE DE LA VILLE LEVEQUE 75008 PARIS"
}
}Cela signifie que address-form ne doit s’afficher que si la case du composant AcceptCGU est cochée.
Idéalement, on voudrait pouvoir supporter des conditions plus complexes, par exemple { form-is-valid: "name of form" }, { localization-is-valid: true }, { age-is-over-18: true }, etc.
Ces conditions supplémentaires ne sont pas à implémenter. Elles sont données uniquement en exemple pour vous donner une idée sur comment le produit devrait évoluer.
https://www.loom.com/share/39a972c1046f4c35b85c476c23279235
- L'ensemble des fonctionnalités, y compris la fonctionnalité optionnelle, a été développé
- Une gestion des erreurs a été implémentée aussi bien dans le mock serveur que la partie frontend
En bonus (Cf. les commits liés à la v2) :
- La homepage de Vite a été remplacée par une page pour accéder directement à 3 routes (2 bonnes et 1 mauvaise) afin d'éviter d'entrer des URLs à la main.
- 6 tests unitaires avec Vitest ont été ajoutés, dont certains avec un mock de la "base de données" pour respecter les bonnes pratiques
- du code a été refactorisé pour améliorer l'implémentation, utilisation de :
<Activity>de React 19.2 pour conserver le state interne deschildrenen cas de masquage- TS-Pattern pour utiliser du pattern matching et ainsi éviter des ternaires imbriquées et/ou des mutations
-
Le projet contient un historique Git propre (idéal pour suivre chaque étape du développement), et se base sur les bonnes pratiques de ces projets :
- Conventional Commits
- commit-message-emoji par défaut, et Gitmoji en complément
-
Comme le projet ne comportait pas de formateur, Biome a été ajouté :
- c'est le nouveau standard pour linter et formater des projets en TypeScript (c'est nettement plus performant que le duo Prettier x ESLint).
- la configuration Biome ajoutée au projet permet, au passage, de bénéficier de règles de lint supplémentaires (à la configuration ESLint).
- l'ensemble des fichiers du projet a été formaté automatiquement (dans un commit dédié), à la fois pour uniformiser la codebase et éviter des diffs de formatage dans des commits d'ajout de fonctionnalité.
-
Comme rien ne faisait mention de la possibilité de changer la configuration initiale du projet, rien n'a été modifié à ce niveau, volontairement :
-
les fichiers
package.json,yarn.lock,tsconfig.json,tsconfig.app.json,vite.config.ts,eslint.config.jssont donc identiques aux fichiers initiaux. -
ESLint fonctionne donc en parallèle de Biome (via les extensions d'IDE respectives). Ceci dit, l'idéal aurait été de supprimer tout ce qui est relatif à ESLint pour des soucis de performance, d'autant qu'il n'y a aucune valeur ajoutée à utiliser ESLint dans ce cas de figure.
-
Biome a été ajouté via mise-en-place, d'où le fichier
mise.toml. Cela évite d'ajouter Biome dans les dépendances du projet (n'obligeant donc personne à utliser Biome et le toochain associé), bien qu'il aurait été préférable de le faire dans un projet standard, notamment pour définir un script de formatage danspackage.json. -
comme le fichier
.gitignoreinitial exclut.vscode/*, l'ajout de.vscode/settings.jsonn'est donc pas tracké. -
le projet commence par un commit d'installation de Vite avec le template
react-ts. Les fichiers initiaux du projet (Cf. l'archive ZIP fournie) ont ensuite été ajoutés (en écrasant donc les fichiers générés par le CLI de Vite) afin de voir le diff Git aussi bien en terme de configuration (Cf. le 2e commit) que de fichiers applicatifs (Cf. le 3e commit). Etant plutôt familier à Next.js, j'avais besoin d'un référentiel (pour ne pas me poser de questions longtemps concernant les fichiers spécifiques au projet). -
l'option TypeScript
erasableSyntaxOnlyayant été activée par défaut, aucun Enum n'a été utilisé dans le projet, le but étant de ne pas ajouter des@ts-expect-errordans la codebase. Ceci dit, il aurait été bienvenu de pouvoir en utiliser 1 ou 2, notamment au lieu des chaînes de caractères associées au typeStepafin d'avoir :enum Step { Initial, Loading, Fetched, }
-
-
Les conventions de nommage ont été respectées concernant tout code ajouté. Par contre, aucun code présent dans les fichiers initiaux n'a été modifié, volontairement (pour rester neutre), sauf en cas de nécessité :
-
la règle
useNamingConventionde Biome ayant été activée (en warning) etAcceptCGUne respectant pas les conventions — Cf. l'erreur :"Two consecutive uppercase characters are not allowed in PascalCase because strictCase is set to
true.biomelint/style/useNamingConvention"on retrouve donc une petite divergence avec des noms de variable comme
isCguAccepted(avecCguet nonCGU— c'est un détail, mais c'est mentionné pour que ce ne soit pas perçu comme un manque de rigueur). Dans une projet standard, tout aurait été uniformisé.REMARQUE :
isCguAcceptedétait initialement nomméechecked. Avec un verbe modal au début pour rappeler le type booléen et un nom plus proche du "domaine", le code devient plus conventionnel.
-
-
les bonnes pratiques de typage ont été appliquées :
- Aucun
any, aucun non-null assertion et aucun casting abusif n'a été ajouté. - L'exploitation optimale de l'inférence et du narrowing, l'utilisation de type guards et la définition de types stricts ont été privilégiés.
- Aucun
-
les bonnes pratiques en JavaScript ont également été appliquées :
- lisibilité
- indentation peu profonde
- complexité raisonnable (ni trop basse ni trop grande)
- early returns
- utilisation d'aucun
let - throw early pour des cas qui ne sont pas censés arriver mais que TypeScript et le bundler ne peuvent pas catcher, afin de ne pas avoir d'erreurs silencieuses.
-
le fichier
intents.tsa finalement été considéré comme une base de données (c'est-à-dire un ESModule avec juste des données), donc la fonctionfetchIntentsa été supprimée en fusionnant avec le code de la fonctionsimulatedFetchcréée via un ESModule dédié. Au passage, une fonctionwaita été extraite danshelpers.ts(en utilisant des constantes pour éviter les magic numbers pour les durées) pour améliorer la lisibilité desimulatedFetch, au même titre que l'utilisation deasync/await(au lieu de la syntaxe classique des promesses). -
la fonction
simulatedFetchprend en compte 2 erreurs différentes (le cas d'une route qui ne correspond pas au pattern et le cas où la "base de données" n'a pas descreen_idcorrespondant à la route), d'autres cas d'erreurs auraient pu être implémentés (comme par exemple le mock d'une erreur réseau via un throw qui s'exécuterait 10% du temps via une fonction aléatoire juste après leawait wait()). Ceci dit, l'idée était d'implémenter une réponse avec 2 types d'objets différents (Cf. les typesResponseValidetResponseError) pour ne pas retourner seulement l'objet des intents, sans pour autant aller jusqu'à générer une réponse HTTP plus complète avec status code et autre. -
des commentaires ont été mis :
- pour expliquer les choix d'implémentation (avec
//) - pour documenter des constantes ou type particuliers (Cf. des JSDoc avec
/** */) - pour aérer et "chapitrer" le code (Cf. les gros blocs avec
/* ******* */)
- pour expliquer les choix d'implémentation (avec
-
au lieu de cette structure de données :
"page-b": { "accept-cgu": { label: "J’accepte les CGU" }, "address-form": { default: "16 RUE DE LA VILLE LEVEQUE 75008 PARIS", "visible-if": { "accept-cgu": true }, }, button: { label: "Envoyer" }, },
il aurait sans doute été préférable d'avoir quelque chose comme cela :
"page-b": [ { name: "accept-cgu", props: { label: "J’accepte les CGU" }, }, { name: "address-form", props: { default: "16 RUE DE LA VILLE LEVEQUE 75008 PARIS" }, conditions: { "visible-if": { "accept-cgu": true } }, }, { name: 'button', props: { label: "Envoyer" }, }, ],
dans le but de :
-
faciliter les traitements côté frontend, en séparant notamment les props des conditions et avec une gestion de la profondeur plus naturelle (ceci dit, le but de l'exercice était certainement d'imposer quelques contraintes pour voir comment on manipule des structures de données différentes).
REMARQUE : La question s'est posée de savoir si, dans le mock "serveur" (dans
simulatedFetch), il ne valait pas mieux faire un traitement pour retourner un objet idéal pourScreenRenderer, mais comme le principal de l'implémentation devait se retrouver dansScreenRenderer, la décision a finalement été de considérersimulatedFetchcomme un simple sélecteur. -
garantir l'ordre des composants via un tableau (c'est plus adapté qu'un objet — Cf. cet article par exemple)
-
améliorer la lisibilité (via les propriétés
name,propsetconditions) au détriment du nombre de caractères.
Si le nombre de caractères est un problème, cette structure (avec des tuples) aurait été bien également :
"page-b": [ [ "accept-cgu", { label: "J’accepte les CGU" }, {}, // avec ou sans cet objet vide, mais c'est mieux avec, si on suit les bonnes pratiques ], [ "address-form", { default: "16 RUE DE LA VILLE LEVEQUE 75008 PARIS" }, { "visible-if": { "accept-cgu": true } }, ], [ 'button', { label: "Envoyer" }, {}, ], ],
-
